Row-level Locks
Introduction
Row-level locks are used to protect specific rows when data is being modified. Unlike table locks, Postgres does any row lock storage outside of RAM, writing them directly into the tuples (data rows) on the hard disk to avoid memory overflow.
Differences
Table-level Lock(8 modes): Protects the table schema and controls broad behaviors, such as full-table reads, table writes, or column alterations. You can imagine these 8 modes as "admission tickets" that control who can enter the building and what they can do inside.Row-level Lock(4 modes): Protects the actual data values inside specific rows to prevent two people from modifying the same record simultaneously. It is like locking individual small rooms inside the building.
Connections
- Although
Table-level LockandRow-level Lockare two distinct sets of lock modes, they always run in parallel and support each other. - When you act on a row, Postgres automatically assigns both a table lock and a row lock under the principle that obtaining a Row Lock requires first creating a Table Lock.
- With this separated coordination mechanism, the 8 table-level lock modes handle keeping the table schema stable, while the 4 row-level lock modes protect rows inside that table from being modified haphazardly.
- For example, when you run the command
UPDATE employee SET name = 'name' WHERE id = 5- At the table level: Postgres automatically assigns a table lock in
ROW EXCLUSIVEmode (1 of the 8 table-level modes). This lock tells the system, "You are modifying a few rows in this table, anyone who wants to modify other rows or read the table can do so freely, but no one is allowed toDROPthe table or alter column structures (ALTER) at this moment." - At the row level: Postgres assigns a row lock in
FOR NO KEY UPDATEmode (1 of the 4 row-level modes) to the row whereid = 5. This lock tells the system, "This row with id = 5 is locked because someone is using it, other transactions that want to modify this row must line up and wait."
- At the table level: Postgres automatically assigns a table lock in
Lock modes
There are the following types:
FOR KEY SHARE: This is the weakest type of Row-level lock- For rows currently being locked
INSERT: allowUPDATE: allow updating regular columns, block updating key columns (Primary key, Foreign key, Unique column)DELETE: block
- For rows with a foreign key linking to the locked rows, it allows
INSERT/UPDATE/DELETE.
- For rows currently being locked
FOR SHARE: This lock is similar toFOR KEY SHAREbut slightly stronger- For rows currently being locked
INSERT: allowUPDATE: block (this is the difference fromFOR KEY SHARE, as it does not allow updating any columns at all)DELETE: block
- For rows with a foreign key linking to the locked rows, it allows
INSERT/UPDATE/DELETE. - Multiple sessions can hold the
FOR KEY SHARE/FOR SHARElock on the same row simultaneously to read together and ensure no data is modified or deleted by others.
- For rows currently being locked
FOR NO KEY UPDATE: Using this key means telling Postgres that you might modify any column of this row except for the primary key column- It is activated automatically when using the
UPDATEstatement for regular columns, not a Primary key or Unique key. - For rows currently being locked
INSERT: allowUPDATE: blockDELETE: block
- For rows with a foreign key linking to the locked rows, it allows
INSERT/UPDATE/DELETE. - Up to this point, you will see it is quite similar to
FOR SHARE, but the difference lies in:- If using
FOR SHARE, it allows multiple rows withFOR KEY SHARE/FOR SHARElocks to coexist. - If using
FOR NO KEY UPDATE, it only allows running alongside theFOR KEY SHARElock, not other locks (FOR SHARE/FOR NO KEY UPDATE/FOR UPDATE).
- If using
- It is activated automatically when using the
FOR UPDATE: Using this key means telling Postgres that you might modify any column of this row, including the primary key column- It is activated automatically when using the
UPDATEstatement for a Primary key or Unique key. - For rows currently being locked
INSERT: allowUPDATE: blockDELETE: block
- You can see that this lock behaves like
FOR NO KEY UPDATEbut is stronger regarding rows with a foreign key linking to the locked rowsINSERT: not allowed to INSERT a column containing a foreign key pointing to the locked row, while inserting the id of unlocked columns is still allowed.UPDATE: not allowed to UPDATE the foreign key column to the value of the locked primary key, while other values are still allowed.DELETE: allow
- Similar to
FOR NO KEY UPDATE, while a row is being locked, you cannot apply any other Row-level lock to any other rows.
- It is activated automatically when using the
Matrix
This is the matrix showing the relationship between Row-level locks
| Current Lock (Tx A) | Update Non-Key (Parent)? | Update Key (Parent)? | FK Link (Child)? | Allowed Locks (Tx B) | Blocked Locks (Tx B) |
| :------------------- | :----------------------: | :------------------: | :--------------: | :------------------------------ | :-------------------------------------- |
| 1. FOR KEY SHARE | ✅ YES | ❌ Blocked | ✅ YES | KEY SHARE, SHARE, NO KEY UPDATE | UPDATE |
| 2. FOR SHARE | ❌ Blocked | ❌ Blocked | ✅ YES | KEY SHARE, SHARE | NO KEY UPDATE, UPDATE |
| 3. FOR NO KEY UPDATE | ❌ Blocked | ❌ Blocked | ✅ YES | KEY SHARE | SHARE, NO KEY UPDATE, UPDATE |
| 4. FOR UPDATE | ❌ Blocked | ❌ Blocked | ❌ Blocked | None (Exclusive) | KEY SHARE, SHARE, NO KEY UPDATE, UPDATE |
Detail
First, let us create the tables as follows
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT
);
CREATE TABLE product_variants (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT REFERENCES products(id),
sku VARCHAR(100) UNIQUE NOT NULL,
variation_name VARCHAR(100) NOT NULL,
price DECIMAL(12, 2) NOT NULL,
stock INT NOT NULL CHECK (stock >= 0)
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
product_variant_id BIGINT REFERENCES product_variants(id),
status VARCHAR(20) NOT NULL,
total_price DECIMAL(12, 2) NOT NULL,
shipping_address TEXT NOT NULL,
);
FOR KEY SHARE
-- Transaction 1
BEGIN;
SELECT * FROM product_variants WHERE id = 1 FOR KEY SHARE;
-- Transaction 2
BEGIN;
-- Statement 1
SELECT * FROM product_variants WHERE id = 1 FOR KEY SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR NO KEY UPDATE;
SELECT * FROM product_variants WHERE id = 1 FOR UPDATE;
-- Statement 2
INSERT INTO product_variants(product_id, sku, variation_name, price, stock) VALUES (1, 'sku-1', 'variation_name', 1000, 100);
UPDATE product_variants SET id = 100 WHERE id = 1;
UPDATE product_variants SET variation_name = 'variation_name' WHERE id = 1;
DELETE FROM product_variants WHERE id = 1;
- Statement 1: You will see that you can select with
FOR KEY SHARE/FOR SHARE/FOR NO KEY UPDATElocks but cannot select with theFOR UPDATElock. - Statement 2: The results are as follows
- Can
INSERTat will - Can
UPDATEregular columns likevariation_namebut cannot update Key columns likeid(primary key) - Cannot
DELETEthe row being blocked
FOR SHARE
-- Transaction 1
BEGIN;
SELECT * FROM product_variants WHERE id = 1 FOR SHARE;
-- Transaction 2
BEGIN;
-- Statement 1
SELECT * FROM product_variants WHERE id = 1 FOR KEY SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR NO KEY UPDATE;
SELECT * FROM product_variants WHERE id = 1 FOR UPDATE;
-- Statement 2
INSERT INTO product_variants(product_id, sku, variation_name, price, stock) VALUES (1, 'sku-1', 'variation_name', 1000, 100);
UPDATE product_variants SET id = 100 WHERE id = 1;
UPDATE product_variants SET variation_name = 'variation_name' WHERE id = 1;
DELETE FROM product_variants WHERE id = 1;
- Statement 1: Can select with
FOR KEY SHARE/FOR SHARElocks but cannot select withFOR NO KEY UPDATE/FOR UPDATElocks. - Statement 2: The results are as follows
- Can
INSERTat will - This is the difference from
FOR KEY SHAREbecause it does not allowUPDATEon any columns at all - Cannot
DELETEthe row being blocked
FOR NO KEY UPDATE
-- Transaction 1
BEGIN;
SELECT * FROM product_variants WHERE id = 1 FOR NO KEY UPDATE;
-- Transaction 2
BEGIN;
-- Statement 1
SELECT * FROM product_variants WHERE id = 1 FOR KEY SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR NO KEY UPDATE;
SELECT * FROM product_variants WHERE id = 1 FOR UPDATE;
-- Statement 2
INSERT INTO product_variants(product_id, sku, variation_name, price, stock) VALUES (1, 'sku-1', 'variation_name', 1000, 100);
UPDATE product_variants SET variation_name = 'variation_name' WHERE id = 1;
DELETE FROM product_variants WHERE id = 1;
-- Statement 3
INSERT INTO orders(product_variant_id, status, total_price, shipping_address) VALUES (1, 'Pending', 100000, 'shipping_address');
INSERT INTO orders(product_variant_id, status, total_price, shipping_address) VALUES (2, 'Pending', 100000, 'shipping_address');
UPDATE orders SET id = 100 WHERE id = 1;
UPDATE orders SET shipping_address = 'shipping_address' WHERE id = 1;
UPDATE orders SET product_variant_id = 2 WHERE id = 1;
UPDATE orders SET product_variant_id = 1 WHERE id = 7;
DELETE FROM orders WHERE id = 1;
- Statement 1: You can only select for
FOR KEY SHARE, cannot select forFOR SHARE, FOR NO KEY UPDATE, FOR UPDATE. - Statement 2: When you execute in transaction 2, you will see that you can
INSERTbut cannotUPDATE/DELETEany columns of the row being locked. - Statement 3: You can
INSERT/UPDATE/DELETEnormally on the table containing a foreign key pointing to the table with locked rows, please pay attention to this part to compare the difference when usingFOR UPDATE.
FOR UPDATE
-- Transaction 1
BEGIN;
SELECT * FROM product_variants WHERE id = 1 FOR UPDATE;
-- Transaction 2
BEGIN;
-- Statement 1
SELECT * FROM product_variants WHERE id = 1 FOR KEY SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR SHARE;
SELECT * FROM product_variants WHERE id = 1 FOR NO KEY UPDATE;
SELECT * FROM product_variants WHERE id = 1 FOR UPDATE;
-- Statement 2
INSERT INTO product_variants(product_id, sku, variation_name, price, stock) VALUES (1, 'sku-1', 'variation_name', 1000, 100);
UPDATE product_variants SET variation_name = 'variation_name' WHERE id = 1;
DELETE FROM product_variants WHERE id = 1;
-- Statement 3
INSERT INTO orders(product_variant_id, status, total_price, shipping_address) VALUES (1, 'Pending', 100000, 'shipping_address');
INSERT INTO orders(product_variant_id, status, total_price, shipping_address) VALUES (2, 'Pending', 100000, 'shipping_address');
UPDATE orders SET id = 100 WHERE id = 1;
UPDATE orders SET shipping_address = 'shipping_address' WHERE id = 1;
UPDATE orders SET product_variant_id = 2 WHERE id = 1;
UPDATE orders SET product_variant_id = 1 WHERE id = 7;
DELETE FROM orders WHERE id = 1;- Statement 1: This is the strongest type of row lock, so it does not allow any other row locks to run concurrently.
- Statement 2: You can
INSERTbut cannotUPDATE/DELETEany columns of the row being locked. - Statement 3: You can
DELETEnormally, but if youINSERT/UPDATEtheproduct_variant_idwith the value that is currently locked, it is not allowed.
Happy coding!
Comments
Post a Comment