Sunday, November 14, 2021

Performace Tuning : LOCKING

 Performace Tuning : LOCKING 

Sometimes a problem that appears to be a performance issue could actually be due to locking.

Particularly if high occurrences of the following wait events are seen in the AWR or SQL Trace / TKPROF :


  • enq: TX - row lock contention
  • enq: TX - allocate ITL entry
  • enq: TM – contention
  • enq: UL - contention

There are three main types of lock that are likely to be encountered, TX, TM and UL.

Here’s a brief explanation of all three.

1) TX (Rows Locks)

A TX enqueue is created as soon as a transaction is started. It is uniquely identified by the rollback segment number, slot number (in rollback segment’s transaction table) and slot number’s sequence number.

The wait event for this lock is normally "enq: TX - row lock contention".

A session, whilst carrying out DML on a row (or SELECT …. FOR UPDATE) can be waiting on a TX for the following main reasons:

Another session is carrying out DML on the requested row. Or has used SELECT … FOR UPDATE.

Another session has inserted the same row (defined by unique key) into the same table.

There are no free ITL (interested transaction list) slots in the block header. i.e. too many concurrent DML transactions are modifying rows in the same block. Increase INITRANS (and/or MAXTRANS) for the segment. Alternatively the number of rows in a block can be reduced by re-organizing the table; either reducing the block size or increasing the PCTFREE. The wait event for this situation is "enq: TX - allocate ITL entry".

2) TM (Table Locks)

This lock is acquired by a transaction when using DML (or SELECT …. FOR UPDATE) or by the LOCK TABLE statement.

DML operations require table locks to reserve DML access to the table on behalf of a transaction and to prevent DDL operations that would conflict with the transaction.

The wait for this lock is "enq: TM – contention".

3)UL (User Lock)

These are user specified locks.

They can be requested and released using the DBMS_LOCK package procedures REQUEST and RELEASE.

The wait for this lock is "enq: UL - contention".


Identifying where locks are being held

The following will identify the locks that have been held for the longest time along with the blocking/blocked session:

SELECT
   lr.sid blocker_sid,
   ld.sid blocked_sid,
   lr.type, -- Type will typically be TM, TX, UL. Otherwise it is system type.
   lt.name,
   lr.id1, -- The value contained in these varies. For TM ID1 contains the object_id, for TX it is the Rollback entry
   lr.id2, -- The value contained in these varies. For TX it is the Transaction entry.
   decode(lr.lmode, 
          0,'none(0)',
          1,'null(NULL)(1)',
          2,'row-S(SS)(2)',
          3,'row-S(SX)(3)',
          4,'Share(S)(4)',
          5,'S/Row-X(SSX)(5)',
          6,'exclusive(X)(6)',
          LTRIM(TO_CHAR(lr.lmode,'990'))) lock_hold_mode,
   decode(lr.request,
          0,'none(0)',
          1,'null(NULL)(1)',
          2,'row-S(SS)(2)',
          3,'row-S(SX)(3)',
          4,'Share(S)(4)',
          5,'S/Row-X(SSX)(5)',
          6,'exclusive(X)(6)',
          LTRIM(TO_CHAR(lr.request,'990'))) lock_request_mode,
   lr.ctime time_blocker_held,
   ld.ctime time_blocked_waiting
FROM v$lock lr, 
     v$lock ld, 
     v$lock_type lt 
WHERE lt.type (+) = lr.type
AND ld.id1 = lr.id1 -- rollback entries match
AND ld.id2 = lr.id2 -- transaction entries match
AND lr.block = 1 -- blocker
AND ld.block = 0 -- blocked
ORDER BY lr.ctime DESC; 
-----------------------------------------------------------------------------------

The following will give the chains of locks - with the longest first:

The key blocking sessions should be able to be identified from this.

Note that if there are occurrences of blocker sessions blocking multiple blocked sessions then parts of the chains will be reported multiple times (there will be one row for each combination of chain starts and ends).

SELECT * 
FROM 
   (SELECT 
       level chainlevel,
       blocked_sid,
       CONNECT_BY_ROOT blocker_sid ultimate_blocker_sid,
       sys_connect_by_path(blocker_sid,'/')|| '/' || blocked_sid blocking_path,
       time_blocked_waiting 
    FROM (SELECT 
             lr.sid blocker_sid, 
             ld.sid blocked_sid, 
             lr.id1,
             lr.id2,
             ld.ctime time_blocked_waiting
          FROM v$lock lr,
               v$lock ld
          WHERE ld.id1 = lr.id1 
          AND ld.id2 = lr.id2 
          AND lr.block = 1 
          AND ld.block = 0) linked_locks 
    CONNECT BY NOCYCLE PRIOR blocked_sid = blocker_sid) chains 
-- blocked end of chain is not a blocker session
WHERE NOT EXISTS (SELECT 'exists' 
                  FROM v$lock lr,
                       v$lock ld
                  WHERE ld.id1 = lr.id1
                  AND ld.id2 = lr.id2
                  AND lr.block = 1
                  AND ld.block = 0
                  AND lr.sid = chains.blocked_sid)
-- blocker end of chain is not a blocked session 
AND NOT EXISTS (SELECT 'exists' 
                FROM v$lock lr,
                     v$lock ld
                WHERE ld.id1 = lr.id1
                AND ld.id2 = lr.id2
                AND lr.block = 1
                AND ld.block = 0
                AND ld.sid = chains.ultimate_blocker_sid)
ORDER BY chainlevel DESC;
-----------------------------------------------------------

The following will report any cycles/deadlocks:

However, deadlock detection should prevent these from occurring.

SELECT * 
FROM (SELECT 
         DECODE(CONNECT_BY_ISCYCLE,1,'YES','NO') is_a_cycle,
         level,
         blocked_sid,
         CONNECT_BY_ROOT blocker_sid ultimate_blocker_sid,
         sys_connect_by_path(blocker_sid,'/')|| '/' || blocked_sid blocking_path,
         time_blocked_waiting
      FROM (SELECT
               lr.sid blocker_sid,
               ld.sid blocked_sid,
               lr.id1,
               lr.id2,
               ld.ctime time_blocked_waiting
            FROM v$lock lr,
                 v$lock ld
            WHERE ld.id1 = lr.id1
            AND ld.id2 = lr.id2
            AND lr.block = 1
            AND ld.block = 0) linked_locks
      CONNECT BY NOCYCLE PRIOR blocked_sid = blocker_sid)
WHERE is_a_cycle = 'YES';
------------------------------------------------------------------------------------
To get more information for any session ids (sid), with the longest running locks first :
SELECT 
   l.sid,
   l.block,
   DECODE(l.block,1,'Blocker','Blocked'),
   s.blocking_session,
   l.type,
   lt.name,
   l.id1,
   l.id2,
   decode(l.lmode,
          0,'none(0)',
          1,'null(NULL)(1)',
          2,'row-S(SS)(2)',
          3,'row-S(SX)(3)',
          4,'Share(S)(4)',
          5,'S/Row-X(SSX)(5)',
          6,'exclusive(X)(6)',
          LTRIM(TO_CHAR(l.lmode,'990')) ) lock_hold_mode,
   decode(l.request,
          0,'none(0)',
          1,'null(NULL)(1)',
          2,'row-S(SS)(2)',
          3,'row-S(SX)(3)',
          4,'Share(S)(4)',
          5,'S/Row-X(SSX)(5)',
          6,'exclusive(X)(6)',
          LTRIM(TO_CHAR(l.request, '990')) ) lock_request_mode,
   l.ctime time_held_s,
   s.seconds_in_wait,
   s.wait_class,
   s.event,
   s.p1text,
   s.p1,
   s.p1raw,
   s.p2text,
   s.p2,
   s.p2raw,
   s.p3text,
   s.p3,
   s.p3raw,
   s.serial#,
   s.program,
   s.client_identifier,
   s.terminal,
   s.command,
   ct.command_name,
   s.service_name,
   s.module,
   s.action,
   s.username,
   s.machine,
   DECODE(l.type, 'TM', o.object_name) object_name,
   s.sql_id,
   st.sql_text, -- just the first 64 characters of the SQL text
   st.hash_value,
   s.row_wait_obj#,
   s.row_wait_file#,
   s.row_wait_block#,
   s.row_wait_row#
FROM v$session s,
     v$lock l,
     v$sqltext st,
     v$lock_type lt,
     v$sqlcommand ct,
     dba_objects o
WHERE s.sid (+) = l.sid
AND (   (l.request != 0) -- either requesting a lock 
      OR 
        (l.lmode != 0 -- or holding a lock
         AND 
         EXISTS (SELECT 'exists' 
                 FROM v$lock ld 
                 WHERE ld.request != 0 
                 AND ld.id1 = l.id1 
                 AND ld.id2 = l.id2) -- and blocking a blocked session
        )
    )
AND st.sql_id (+) = s.sql_id
AND st.piece (+) = 0
AND o.object_id (+) = l.id1
AND lt.type (+) = l.type
AND ct.command_type (+) = s.command
-- AND l.sid = <session id>
ORDER BY l.ctime DESC;
--------------------------------------------------------------

The object and rowid for a particular blocked session can be determined by using the following SQL:

This is primarily for row locks (TX), but it may also show some relevant information for TM locks, depending on the mode the table was locked and the activity taking place on the locked session.


SELECT
   o.object_name,
   s.row_wait_obj#,
   s.row_wait_file#,
   s.row_wait_block#,
   s.row_wait_row#,
   dbms_rowid.rowid_create( 0, s.row_wait_obj#, s.row_wait_file#, s.row_wait_block#, s.row_wait_row#) restricted_rowid,
   dbms_rowid.rowid_create( 1, s.row_wait_obj#, s.row_wait_file#, s.row_wait_block#, s.row_wait_row#) extended_rowid
FROM v$session s,
     dba_objects o
WHERE s.row_wait_obj# = o.object_id (+)
AND s.sid = <sid>;
-------------------------------------------------------------

The rowid could be used to query the contents of the locked row on the object (table).

The following SQL can be run for particular sql ids to get more SQL text:

SELECT
   sql_text -- First 1000 characters
FROM v$sql
WHERE sql_id = <sql_id>;
or
SELECT
   sql_text
FROM v$sqltext
WHERE sql_id = <sql_id>
ORDER BY piece;
------------------------------------------------------------

5 Identifying where locks were held

The information on the views v$lock, v$session is only for current sessions only.

However, it is possible that the Active Session History (ASH) may have captured information on locks held for more than a few seconds.

The following query will give information on all sessions from the Active Session History (on DBA_ACTIVE_SESS_HISTORY) that were blocked due to a Row, Table or User lock.

A similar query using view V$ACTIVE_SESSION_HISTORY could be used to check the very recent history (not yet flushed to DBA_ACTIVE_SESS_HISTORY).

SELECT
   s.session_id,
   s.blocking_session,
   s.session_serial#,
   s.sql_id,
   s.wait_class,
   s.event,
   s.p1text,
   s.p1,
   s.p2text,
   s.p2,
   s.p3text,
   s.p3,
   o.object_type,
   o.object_name,
   s.current_obj#,
   s.current_file#,
   s.current_block#,
   s.current_row#,
   s.program,
   s.module,
   s.action,
   s.client_id,
   s.machine,
   COUNT(*)*10 approx_wait_secs, -- note that if 10 seconds is reported then it could be a lot less
   MIN(s.sample_time) start_sample_time,
   MAX(s.sample_time) end_sample_time
FROM dba_active_sess_history s,
     dba_hist_seg_stat_obj o 
WHERE s.dbid = <db_id>
AND s.instance_number = <inst_num>
AND s.snap_id BETWEEN <begin_snap> and <end_snap>
AND o.dbid (+) = s.dbid
AND o.obj# (+) = s.current_obj#
AND s.blocking_session IS NOT NULL
AND s.event IN 
    ('enq: TX - row lock contention'
    ,'enq: TM - contention'
    ,'enq: UL - contention'
    ,'enq: TX - allocate ITL entry')
GROUP BY s.session_id, s.blocking_session, s.session_serial#,
 s.sql_id, s.wait_class, s.event, s.p1text, s.p1, s.p2text, s.p2, s.p3text, s.p3,
 o.object_type, o.object_name, s.current_obj#, s.current_file#, 
s.current_block#, s.current_row#, 
s.program, s.module, s.action, s.client_id, s.machineORDER BY COUNT(*) DESC;
-----------------------------------------------------------------------------------------------
The following query will give information on chains of locks (due to table, row or user locks) that have occurred for more than a few seconds, with the longest chains first :

SELECT 
   level,
   sample_time,
   session_id blocked_sid,
   CONNECT_BY_ROOT blocking_session ultimate_blocker_sid,
   sys_connect_by_path(blocking_session,'/')|| '/' || session_id blocking_path 
FROM
   (-- Blocked Sessions
    SELECT
       s.session_id,
       s.blocking_session,
       s.sample_time
    FROM dba_active_sess_history s 
    WHERE s.dbid = <db_id>
    AND s.instance_number = <inst_num>
    AND s.snap_id BETWEEN <begin_snap> and <end_snap>
    AND s.blocking_session IS NOT NULL
    AND s.event IN 
        ('enq: TX - row lock contention'
        ,'enq: TM - contention'
        ,'enq: UL - contention'
        ,'enq: TX - allocate ITL entry')
    UNION
    -- Blocking Sessions
    SELECT s.session_id,
           s.blocking_session,
           s.sample_time
    FROM dba_active_sess_history s 
    WHERE s.dbid = <db_id>
    AND s.instance_number <inst_num>
    AND s.snap_id BETWEEN <begin_snap> and <end_snap>
    AND s.blocking_session IS NULL
    AND s.event IN
        ('enq: TX - row lock contention'
        ,'enq: TM - contention'
        ,'enq: UL - contention'
        ,'enq: TX - allocate ITL entry')
    AND EXISTS
       (SELECT 'exists' 
        FROM DBA_HIST_ACTIVE_SESS_HISTORY bs 
        WHERE bs.dbid = <db_id>
        AND bs.instance_number = <inst_num>
        AND bs.snap_id BETWEEN <begin_snap> and <end_snap>
        AND bs.blocking_session = s.session_id
        AND bs.sample_time = s.sample_time
        AND bs.blocking_session IS NOT NULL
        AND bs.event IN 
            ('enq: TX - row lock contention'
            ,'enq: TM - contention'
            ,'enq: UL - contention'
            ,'enq: TX - allocate ITL entry')
       )
   )
CONNECT BY NOCYCLE PRIOR session_id = blocking_session 
                   AND PRIOR sample_time = sample_time 
ORDER BY level DESC, blocked_sid, sample_time;
----------------------------------------------------------------

6 "PL/SQL lock timer" waits

In this case there is no lock or interaction between sessions.

A session simply sleeps for a pre-determined length of time specified in a call to the USER_LOCK.SLEEP or DBMS_LOCK.SLEEP procedures.

However it can appear that a session is locked or there is a performance issue.

The details of sessions with "PL/SQL lock timer" waits can be queried using the following SQL:


SELECT
   s.sid,
   s.seconds_in_wait,
   s.wait_class,
   s.event,
   s.p1text,
   s.p1,
   s.p1raw,
   s.serial#,
   s.program,
   s.client_identifier,
   s.terminal,
   s.command,
   ct.command_name,
   s.service_name,
   s.module,
   s.action,
   s.username,
   s.machine,
   s.sql_id,
   st.sql_text, -- just the first 64 characters of the SQL text
   st.hash_value
FROM v$session s,
     v$sqltext st,
     v$sqlcommand ct
WHERE st.sql_id (+) = s.sql_id
AND st.piece (+) = 0
AND ct.command_type (+) = s.command
AND s.event = 'PL/SQL lock timer'
ORDER BY s.seconds_in_wait DESC;

No comments: