In Oracle 23c we can use Lock-Free Reservations to reduce the incidents of blocking sessions for frequently updated numeric column values.
Relational databases have a heavy focus on ACID compliance. This stops logical data corruption, which is really important for most of our data. We would rather have sessions block each other, than risk having incorrect data in our tables.
Despite this, there are some scenarios where blocking sessions are problematic and unnecessary. The main example in this article uses a column that is frequently incremented to maintain sales total counters.
◉ The Problem : Blocking Sessions
We create a table to hold product information and seed it with some products. We include an ITEMS_SOLD column, which provides a running total of the sales as a summary, so we don't have to aggregate the raw data.
-- Clean up before doing a new run.
drop trigger if exists sales_trg;
drop table if exists sales purge;
alter table if exists products modify (items_sold not reservable);
drop table if exists products purge;
create table products (
product varchar2(10) primary key,
items_sold number
);
insert into products (product, items_sold)
values ('banana', 0),
('apple', 0),
('lemon', 0),
('lime', 0);
commit;
We create a table to hold sales information for our products. Each sale is a new insert, so we don't have any problems with updates getting blocked because of heavy transaction rates.
create table sales (
id number generated always as identity primary key,
product varchar2(10),
quantity number,
constraint sales_products_fk foreign key (product) references products (product)
);
We would expect our application to update the PRODUCTS table for every new sale to keep the ITEMS_SOLD column up to date. In this case we will implement that using a trigger.
create or replace trigger sales_trg
after insert on sales
for each row
begin
update products p
set p.items_sold = p.items_sold + :new.quantity
where p.product = :new.product;
end;
/
With each insert into the SALES table, our PRODUCTS table is updated. We check our current totals.
select * from products;
PRODUCT ITEMS_SOLD
---------- ----------
banana 0
apple 0
lemon 0
lime 0
SQL>
We create some new sales. The trigger fires during the inserts, and we see the impact immediately in the ITEMS_SOLD column values.
insert into sales (product, quantity) values ('banana', 10);
insert into sales (product, quantity) values ('apple', 5);
select * from products;
PRODUCT ITEMS_SOLD
---------- ----------
banana 10
apple 5
lemon 0
lime 0
SQL>
We commit the new sales.
commit;
The problem with this setup is inserts from different sessions can block each other. Open two new connections to the database. In session 1 we issue the following statement.
insert into sales (product, quantity) values ('banana', 1);
In session 2 we issue the following statement.
insert into sales (product, quantity) values ('banana', 5);
Notice the insert in session 2 is blocked until session 1 issues a commit or rollback. This is because both sessions are attempting to update the same row in the PRODUCTS table via the trigger.
In session 1 we issue a commit, and we see the insert in session 2 completes. Issue a commit in session 2 also.
-- Session 1
commit;
-- Session 2
commit;
select * from products;
PRODUCT ITEMS_SOLD
---------- ----------
banana 16
apple 5
lemon 0
lime 0
SQL>
◉ The Solution : Lock-Free Reservations
Lock-free reservations stop the sessions from blocking each other. Transactions against a reservable column are held in a reservation journal and only processed on commit of the transaction.
Let's clean up the SALES and PRODUCTS tables.
truncate table sales;
update products
set items_sold = 0;
commit;
We set the ITEMS_SOLD column to RESERVABLE.
alter table if exists products modify (items_sold reservable);
We check the USER_OBJECTS view and we see a reservation journal table has been created.
select object_name
from user_objects
where object_type = 'TABLE';
OBJECT_NAME
--------------------------------------------------------------------------------
PRODUCTS
SALES
SYS_RESERVJRNL_87694
SQL>
We describe this reservation journal table and we see it includes the primary key column (PRODUCT), the operation on the ITEMS_SOLD column (ITEMS_SOLD_OP) and the reserved value for the operation on the ITEMS_SOLD column (ITEMS_SOLD_RESERVED).
desc sys_reservjrnl_87694
Name Null? Type
----------------------------------------- -------- ----------------------------
ORA_SAGA_ID$ RAW(16)
ORA_TXN_ID$ RAW(8)
ORA_STATUS$ CHAR(12)
ORA_STMT_TYPE$ CHAR(16)
PRODUCT NOT NULL VARCHAR2(10)
ITEMS_SOLD_OP CHAR(7)
ITEMS_SOLD_RESERVED NUMBER
SQL>
We create some new sales. The trigger fires during the inserts, but we don't see the impact immediately in the ITEMS_SOLD column values.
insert into sales (product, quantity) values ('banana', 10);
insert into sales (product, quantity) values ('apple', 5);
select * from products;
PRODUCT ITEMS_SOLD
---------- ----------
banana 0
apple 0
lemon 0
lime 0
SQL>
We check the reservation journal table and we can see the two reservations for the updates to the PRODUCTS table.
select ora_stmt_type$,
product,
items_sold_op,
items_sold_reserved
from sys_reservjrnl_87694;
ORA_STMT_TYPE$ PRODUCT ITEMS_S ITEMS_SOLD_RESERVED
---------------- ---------- ------- -------------------
UPDATE apple + 5
UPDATE banana + 10
SQL>
We issue a commit, and we see the changes to the PRODUCTS table, and the journal table is empty.
commit;
select * from products;
PRODUCT TOTAL_QUANTITY
---------- --------------
banana 10
apple 5
lemon 0
lime 0
SQL>
select ora_stmt_type$,
product,
items_sold_op,
items_sold_reserved
from sys_reservjrnl_87694;
no rows selected
SQL>
Open two connections to the database. In session 1 we issue the following statement.
insert into sales (product, quantity) values ('banana', 1);
In session 2 we issue the following statement.
insert into sales (product, quantity) values ('banana', 5);
The insert in session 2 is no longer blocked by the first session. Both still performed their updates, but the trigger changes to the ITEMS_SOLD column in the PRODUCTS table were reserved.
We don't see any changes to the ITEMS_SOLD column until the sessions commit their changes.
select * from products;
PRODUCT TOTAL_QUANTITY
---------- --------------
banana 10
apple 5
lemon 0
lime 0
SQL>
-- Session 2
commit;
select * from products;
PRODUCT TOTAL_QUANTITY
---------- --------------
banana 15
apple 5
lemon 0
lime 0
SQL>
-- Session 1
commit;
select * from products;
PRODUCT TOTAL_QUANTITY
---------- --------------
banana 16
apple 5
lemon 0
lime 0
SQL>
◉ Basic Usage
There are a lot of guidelines and restrictions associated with reservable columns, listed here, but we'll run through some basic usage below.
The table must have a primary key.
drop table if exists t1 purge;
create table t1 (
code varchar2(10),
res_col number reservable
);
*
ERROR at line 1:
ORA-55728: Reservable column property can only be specified for a column on a
table that has a primary key.
SQL>
We repeat the table creation, but this time give the table a primary key and populate it with a row of data.
drop table if exists t1 purge;
create table t1 (
code varchar2(10) primary key,
res_col number reservable
);
insert into t1 values ('one', 0);
commit;
We can't update the column to set a specific value. We must use a + or - operation.
update t1
set res_col = 5
where code = 'one';
*
ERROR at line 1:
ORA-55746: Reservable column update statement only supports + or - operations
on a reservable column.
SQL>
As expected, we can increment or decrement the value using a + or - operation.
update t1
set res_col = res_col + 5
where code = 'one';
update t1
set res_col = res_col - 5
where code = 'one';
commit;
We can't perform any other mathematical operations on it.
update t1
set res_col = res_col * 5
where code = 'one';
*
ERROR at line 1:
ORA-55746: Reservable column update statement only supports + or - operations
on a reservable column.
SQL>
We have to reference the primary key when doing an update. If we omit the primary key, we get an error.
update t1
set res_col = res_col - 5;
*
ERROR at line 1:
ORA-55732: Reservable column update should specify all the primary key columns
in the WHERE clause.
SQL>
We can't delete a row from a table with a reservable column if there are any outstanding reservations. We update the reservable column them attempt a delete, which fails. If we commit or rollback the update, the delete operation works fine.
update t1
set res_col = res_col + 5
where code = 'one';
delete from t1 where code = 'one';
*
ERROR at line 1:
ORA-55754: Resource busy error is detected for the reservable column update
statement. A delete or a DDL operation is conflicting with this update
statement.
SQL>
commit;
delete from t1 where code = 'one';
1 row deleted.
SQL>
We can't drop the table if it has a reservable column.
drop table if exists t1 purge;
*
ERROR at line 1:
ORA-55764: Cannot DROP or MOVE tables with reservable columns. First run "ALTER
TABLE MODIFY ( NOT RESERVABLE)" and then
DROP or MOVE the table.
SQL>
Instead we have to change the column to non reservable first, then drop the table.
alter table if exists t1 modify (res_col not reservable);
drop table if exists t1 purge;
◉ Considerations
Some things to consider about this functionality.
- There are a lot of guidelines and restrictions associated with reservable columns, listed here.
- This functionality is focussed on preventing blocking row locks on hot columns in a table.
- There is more work happening to complete the update processing. For a typical update there is now the maintenance of reservation journal and the final update.
- If there are multiple updates to the same row by our session in a single transaction, this will result in multiple journal entries for that row. When the final update is applied to the table, the multiple journal entries will be consolidated and applied to the destination table using a single update.
- Rollback and rollback to save points are supported as normal.
Source: oracle-base.com
0 comments:
Post a Comment