Db2

Db2

Connect with Db2, Informix, Netezza, open source, and other data experts to gain value from your data, share insights, and solve problems.

 View Only

Have lock, seek row: A handy function to retrieve a row, given a lock 

Tue March 10, 2020 07:37 PM

Posted by: Serge Rielau

DB2 provides a  whole set of tools to analyze locks, but what is missing is a way to connect LOCKs easily with actual rows.

So in this post I will try to fill that gap.
As always we start with a simple running example.
Given a PRODUCTS table with some rows in it we will lock a row and then try to find it again.
CREATE TABLE products(prodid   INTEGER NOT NULL PRIMARY KEY,
                      prodname VARCHAR(20),
                      prodline VARCHAR(20))
        PARTITION BY RANGE (prodline) (PARTITION A_TO_L STARTING MINVALUE ENDING AT 'L' EXCLUSIVE,
                                       PARTITION L_TO_Z STARTING FROM 'L' ENDING AT MAXVALUE);
                                      
INSERT INTO products(prodid, prodname, prodline) VALUES
(1, 'Computer',       'Electronics'),
(2, 'Vacuum Cleaner', 'Appliances'),
(3, 'Pants',          'Clothing'),
(4, 'Washer Fluid',   'Automotive'),
(5, 'Pills',          'Drugs'),
(6, 'Teddy Bear',     'Toys'),
(7, 'Doll',           'Toys');


Note that I chose to range partition the table. This is because partitioning is relevant for rowids and therefore locks.
We need to pick a guinea pig - in this case the teddy bear shall be locked.
When using the command line processor(CLP) we must keep in mind that we switch auto-commit off to keep locks around to play with.
UPDATE COMMAND OPTIONS USING C OFF;

SELECT RID(products) as RID, products.* FROM products WHERE prodid = 6 WITH RS USE AND KEEP UPDATE LOCKS;
RID                  PRODID      PRODNAME             PRODLINE
-------------------- ----------- -------------------- --------------------
     281474976710660           6 Teddy Bear           Toys

  1 record(s) selected.
There are many ways to retrieve held locks within DB2. One such way is the db2pd utility which you can execute from a shell:
db2pd -db test -locks;

Database Partition 0 -- Database TEST -- Active -- Up 0 days 08:40:36 -- Date 12/18/2011 18:17:18

Locks:
Address    TranHdl    Lockname                   Type       Mode Sts Owner      Dur HoldCount  Att        ReleaseFlg rrIID
0x7F58EB80 2          FAFF0080040000000000010052 Row        ..U  G   2          1   0          0x00000000 0x00000001 0
0x7F586580 2          4141414141464462081C83B441 Internal P ..S  G   2          1   0          0x00000000 0x40000000 0
0x7F585A80 2          FAFF0080000000000000FFFF54 Table      .IX  G   2          1   0          0x00002000 0x00000001 0
0x7F581B80 2          FAFF0080000000000000010054 Partition  .IX  G   2          1   0          0x00002000 0x00000001 0
Another way is the MON_GET_LOCKS() table function. It allows for some powerful filtering. In this case we will limit ourselves to locks held by our own session.
The MON_GET_APPLICATION_HANDLE() function will do this:
SELECT LOCK_NAME, MEMBER 
FROM TABLE(MON_GET_LOCKS('<application_handle>' || MON_GET_APPLICATION_HANDLE() || '</application_handle>',
-2));
LOCK_NAME                        MEMBER
-------------------------------- ------
FAFF0080040000000000010052            0
01000000010000000100A04956            0
4141414141464462081C83B441            0
FAFF0080000000000000FFFF54            0
FAFF0080000000000000010054            0

  5 record(s) selected.
Note that locks are local to members. That is, if you are using database partitioning or DB2 pureScale then you should be mindful as to which member a lock originated from.

No matter where you got the lock from, its value is cryptic.
As external users we should refrain from trying to decode the lock value since the encoding may change over time.
For that reason DB2 provides the MON_FORMAT_LOCK_NAME() function.
Given a lock this function will return a set of rows with the components of the lock and their values:
SELECT VARCHAR(name, 30) AS name, VARCHAR(value, 30) AS value
FROM TABLE(MON_FORMAT_LOCK_NAME('FAFF0080040000000000010052'));

NAME                           VALUE
------------------------------ ------------------------------
LOCK_OBJECT_TYPE               ROW
ROWID                          4
DATA_PARTITION_ID              1
PAGEID                         0
TABSCHEMA                      SERGE
TABNAME                        PRODUCTS

  6 record(s) selected.
We are still far away from finding that teddy-bear again. What does all this mean?
  • LOCK_OBJECT_TYPE
    There are different types of locks, such as ROW, TABLE and others.
    We are interested in ROW locks since we are trying to find a locked row
  • TABSCHEMA, TABNAME
    This is self explanatory I hope
  • DATA_PARTITION_ID
    This value specifies within which partition in a range partitioned table the row can be found.
    In our case this would be the L_Z partition. 
  • PAGEID
    This provides us with the page within the partition. 
  • ROWID
    This is really the slot id. that is it is the  number of the row within the page.
The RID of the row we locked was 281474976710660. How do we get there giving those pieces?
The formula is:
DATA_PARTITION_ID * POWER(2,48) + PAGEID * POWER(2,16) + ROWID.
These are a lot of numbers and it's hard to remember. So before doing anything else we will harden this computation into a function:
CREATE OR REPLACE FUNCTION TO_RID(DATA_PARTITION_ID INTEGER, 
PAGEID BIGINT,
ROWID INTEGER)
  RETURNS BIGINT
  SPECIFIC TO_RID
  CONTAINS SQL DETERMINISTIC NO EXTERNAL ACTION
   RETURN DATA_PARTITION_ID * POWER(BIGINT(2),48)
        + PAGEID * POWER(BIGINT(2),16)
+ ROWID;

VALUES TO_RID(1, 0, 4);
1
--------------------
     281474976710660

  1 record(s) selected.
Having retrieved the RID from the lock we can now  retrieve the row:
SELECT * FROM products WHERE RID(products) =  281474976710660;
PRODID      PRODNAME             PRODLINE
----------- -------------------- --------------------
          6 Teddy Bear           Toys

  1 record(s) selected.
All the pieces are now there. But our query to retrieve the row as incomplete because the RID is not unique across members.
So, instead of having to remember all the steps and missing things it will be better to have a single procedure that takes us straight from the lock and a member to the row.
This is what LOCKTOROW does:
--#SET TERMINATOR @
CREATE OR REPLACE PROCEDURE LOCKTOROW(IN  lock      VARCHAR(32),
                                      OUT tabschema VARCHAR(128),
                                      OUT tabname   VARCHAR(128),
                                      IN  mode      VARCHAR(10) DEFAULT ('SHORT'),
                                      IN  member    SMALLINT    DEFAULT (0))
 SPECIFIC LOCKTOROW READS SQL DATA NOT DETERMINISTIC NO EXTERNAL ACTION
 DYNAMIC RESULT SETS 1
BEGIN
  DECLARE stmttxt           VARCHAR(32000);
  DECLARE colname           VARCHAR(128);
  DECLARE lock_object_type  VARCHAR(255);
  DECLARE data_partition_id BIGINT;
  DECLARE rid               BIGINT DEFAULT NULL;
  DECLARE pageid            BIGINT;
  DECLARE stmt              STATEMENT;
  DECLARE res               CURSOR WITH RETURN TO CALLER FOR stmt;

  SET mode = UPPER(mode);
  IF mode NOT IN ('LONG', 'SHORT') THEN
    SIGNAL SQLSTATE '78000' SET MESSAGE_TEXT = 'Unknown mode [LONG|SHORT]';
  END IF;
 
  SELECT MAX(CASE WHEN NAME = 'LOCK_OBJECT_TYPE'  THEN VALUE END),
         MAX(CASE WHEN NAME = 'ROWID'             THEN VALUE END),
         MAX(CASE WHEN NAME = 'TABSCHEMA'         THEN VALUE END),
         MAX(CASE WHEN NAME = 'TABNAME'           THEN VALUE END),
         MAX(CASE WHEN NAME = 'DATA_PARTITION_ID' THEN VALUE END),
         MAX(CASE WHEN NAME = 'PAGEID'            THEN VALUE END)
    INTO lock_object_type, rid, tabschema, tabname, data_partition_id, pageid
    FROM TABLE(MON_FORMAT_LOCK_NAME(lock));

  IF rid IS NULL THEN
    SIGNAL SQLSTATE '78000' SET MESSAGE_TEXT = 'Lock not found';
  END IF;

  IF lock_object_type <> 'ROW' THEN
    SIGNAL SQLSTATE '78000' SET MESSAGE_TEXT = 'Not a ROW lock';
  END IF;

  SET colname = (SELECT colname
                   FROM SYSCAT.COLUMNS
                   WHERE TABNAME   = locktorow.tabname
                        AND TABSCHEMA = locktorow.tabschema
                      AND colno = 0);

  IF mode = 'LONG' THEN                     
    SET stmttxt = 'SELECT * FROM "' || tabschema || '"."' || tabname || '" AS T'
               || ' WHERE RID(T) = ?'
                 || '   AND DBPARTITIONNUM(T."' || colname || '") = ? WITH UR';
  ELSE
    SET stmttxt = 'SELECT ';
    FOR col AS SELECT COLNAME,
                      TYPESCHEMA,
                      TYPENAME,
                      LENGTH                     
                 FROM SYSCAT.COLUMNS
                 WHERE TABNAME   = locktorow.tabname
                      AND TABSCHEMA = locktorow.tabschema
                 ORDER BY colno
    DO
      IF TYPESCHEMA = 'SYSIBM  '
         AND TYPENAME IN ('VARCHAR', 'VARGRAPHIC', 'CLOB', 'BLOB', 'DBCLOB')
         AND LENGTH > 30
      THEN
        SET stmttxt = stmttxt || 'SUBSTR("' || colname || '", 1, 30) AS "' || colname || '", ';
      ELSE   
        SET stmttxt = stmttxt || '"' || colname || '", ';
      END IF;
    END FOR;

    SET stmttxt = SUBSTR(stmttxt, 1, LENGTH(stmttxt) - 2)
               || ' FROM "' || tabschema || '"."' || tabname || '" AS T'
               || '  WHERE RID(T) = ?'
                 || '    AND DBPARTITIONNUM(T."' || colname || '") = ? WITH UR';
  END IF;
  PREPARE stmt FROM stmttxt;
  OPEN res USING data_partition_id * power(bigint(2),48)
                   + pageid * power(bigint(2),16) + rid,
                 member;
END
@
--#SET TERMINATOR ;

CALL LOCKTOROW('FAFF0080040000000000010052', ?, ?);

  Value of output parameters
  --------------------------
  Parameter Name  : TABSCHEMA
  Parameter Value : SERGE

  Parameter Name  : TABNAME
  Parameter Value : PRODUCTS


  Result set 1
  --------------

  PRODID      PRODNAME             PRODLINE
  ----------- -------------------- --------------------
            6 Teddy Bear           Toys

  1 record(s) selected.
This is nice. The procedure has a couple of twists:
  1. There is an optional MODE parameter. It takes either LONG or SHORT.
    The default is SHORT. In SHORT mode the row will be shortened by way of stripping all string or LOB types down to 30 bytes
  2. The MEMBER parameter is optional as well and defaulted to 0.
    So if you are not on DB2 pureScale or in a multi-member warehouse there is no need to specify it.
So the full invocation of the procedure is:
CALL LOCKTOROW('FAFF0080040000000000010052', ?, ?, 'LONG', 0);

  Value of output parameters
  --------------------------
  Parameter Name  : TABSCHEMA
  Parameter Value : SERGE

  Parameter Name  : TABNAME
  Parameter Value : PRODUCTS


  Result set 1
  --------------

  PRODID      PRODNAME             PRODLINE
  ----------- -------------------- --------------------
            6 Teddy Bear           Toys

  1 record(s) selected.

  Return Status = 0 

#Db2

Statistics
0 Favorited
13 Views
0 Files
0 Shares
0 Downloads