Partition table locks and how to add partitions

Create partition

#Create partition partition table
postgres=# create table t_partition (p timestamp,v1 varchar(2),v2 text) partition by range (p);
CREATE TABLE
#Create three partitions
postgres=# create table p0 partition of t_partition for values from ('2022-3-13') to ('2022-3-14');
CREATE TABLE

postgres=# create table p1 partition of t_partition for values from ('2022-3-14') to ('2022-3-15');
CREATE TABLE

postgres=# create table p2 partition of t_partition for values from ('2022-3-15') to ('2022-3-16');
CREATE TABLE

Verification 1: Query how many locks will be added to certain partition locks

session1 pid=12962 starts the transaction and queries the partition p0

postgres=# begin;
BEGIN
postgres=*# select *from t_partition where p>='2022-3-13' and p<'2022-3-14';
 p | v1 | v2
--- + ---- + ----
(0 rows)

Check lock status

It was found that t_partition and partition p0 were locked with accesssharelock
postgres=# select relation::regclass,locktype,transactionid,pid,mode,granted from pg_locks;
  relation | locktype | transactionid | pid | mode | granted
------------- + ------------ + --------------- + ------- + ----------------- + ---------
 p0 | relation | | 12962 | AccessShareLock | t
 t_partition | relation | | 12962 | AccessShareLock | t
             | virtualxid | | 12962 | ExclusiveLock | t
 pg_locks | relation | | 23195 | AccessShareLock | t
             | virtualxid | | 23195 | ExclusiveLock | t
(5 rows)

sesssion2 pid=34336 truncate p0

postgres=# truncate table p0;

Card owner here

Check lock status

postgres=# select relation::regclass,locktype,transactionid,pid,mode,granted from pg_locks;
  relation | locktype | transactionid | pid | mode | granted
------------- + --------------- + --------------- + ---- --- + --------------------- + ---------
             | virtualxid | | 34336 | ExclusiveLock | t
 t_partition | relation | | 12962 | AccessShareLock | t
             | virtualxid | | 12962 | ExclusiveLock | t
 pg_locks | relation | | 23195 | AccessShareLock | t
             | virtualxid | | 23195 | ExclusiveLock | t
 p0 | relation | | 12962 | AccessShareLock | t
 p0 | relation | | 34336 | AccessExclusiveLock | f
             | transactionid | 20397 | 34336 | ExclusiveLock | t

Session 1: Two locks were added, the main table AccessShareLock and a subpartition AccessShareLock

Conclusion: When querying a partitioned table, the main table and sub-partitions within the query range will be locked by AccessSharelock. Prevent other levels of mutex locks, such as truncate locks

Verification 2: Whether partitions can be added normally during the query process of the partition table

session1 remains unchanged
session2 Muscle Building Partition Operation

postgres=# create table p3 partition of t_partition for values from ('2022-3-16') to ('2022-3-17');

Card owner here

Check the lock status

postgres=# select relation::regclass,locktype,transactionid,pid,mode,granted from pg_locks;
  relation | locktype | transactionid | pid | mode | granted
------------- + --------------- + --------------- + ---- --- + --------------------- + ---------
             | virtualxid | | 34336 | ExclusiveLock | t
             | virtualxid | | 12962 | ExclusiveLock | t
 pg_locks | relation | | 23195 | AccessShareLock | t
             | virtualxid | | 23195 | ExclusiveLock | t
 p0 | relation | | 12962 | AccessShareLock | t
 t_partition | relation | | 12962 | AccessShareLock | t
             | transactionid | 20398 | 34336 | ExclusiveLock | t
 t_partition | relation | | 34336 | AccessExclusiveLock | f
             | object | | 34336 | AccessShareLock | t
(9 rows)

Session 2 can be found:
(1) When adding a partition, AccessExclusiveLock will be added to the main table t_partition, the lock of the new object (p3)
AccessShareLock, and transaction lock ExclusiveLock.
(2) pid=34336 has 3 more locks, and Mode=AccessExclusiveLock lock mode
granted=f, that is, the lock conflicts with the AccessShareLock of session 1, resulting in waiting.

in conclusion:
(1) When querying a partition table, AccessSharelock locks will be added to the main table and sub-partitions within the query range, and new partitions cannot be added.
(2) During the process of adding partitions, AccessExclusiveLock will be added to the main table, causing other sessions to be unable to access the sub-partition table through the main table (direct access to the partition table is no problem).
for example:

During session 1 partition creation:
create table p4 partition of t_partition for values from ('2022-3-17') to ('2022-3-18');

Session 2 queries the subpartition through the main table: the session must wait
select *from t_partition where p>='2022-3-13' and p<'2022-3-14';


Session 3 queries subpartitions directly: no wait
select * from p0;

How to add partitions during main table query

session1 remains
sesssion2 adds partition

ppostgres=# create table p4(like t_partition including defaults including constraints);
CREATE TABLE
postgres=# begin;
BEGIN
postgres=*# alter table t_partition attach partition p4 for values from ('2022-3-17') to ('2022-3-18');
ALTER TABLE


Add partition completed

Check lock status

postgres=# select relation::regclass,locktype,transactionid,pid,mode,granted from pg_locks;
  relation | locktype | transactionid | pid | mode | granted
------------- + --------------- + --------------- + ---- --- + -------------------------- + ---------
             | virtualxid | | 34336 | ExclusiveLock | t
 p0 | relation | | 12962 | AccessShareLock | t
 t_partition | relation | | 12962 | AccessShareLock | t
             | virtualxid | | 12962 | ExclusiveLock | t
 pg_locks | relation | | 23195 | AccessShareLock | t
             | virtualxid | | 23195 | ExclusiveLock | t
             | transactionid | 20405 | 34336 | ExclusiveLock | t
 t_partition | relation | | 34336 | ShareUpdateExclusiveLock | t
 p4 | relation | | 34336 | AccessExclusiveLock | t
(9 rows)

Session 2 has additional locks ShareUpdateExclusiveLock of t_partition and AccessExclusiveLock of p4.
Since ShareUpdateExclusiveLock and Session 1's AccessShareLock are not mutually exclusive the success can be increased.

The reason why the partition can be added successfully is that ShareUpdateExclusiveLock is added to the main table during attach.

The impact of partition table truncate

session1 (query partition p0):
postgres=# begin;
BEGIN
postgres=*# select * from t_partition where p>='2022-3-13' and p<'2022-3-14';
          p | v1 | v2
--------------------- + ---- + ------
 2022-03-13 00:00:00 | 1 | aaaa
(1 row)

postgres=*# select relation::regclass,mode,pid from pg_locks;
  relation | mode | pid
------------- + ----------------- + -------
 pg_locks | AccessShareLock | 64609
 p0 | AccessShareLock | 64609
 t_partition | AccessShareLock | 64609
             | ExclusiveLock | 64609
(4 rows)


session2(truncate partition p1):
postgres=*# truncate table p1;
TRUNCATE TABLE
No card owner

session3(truncate partition p0):
postgres=*# truncate table p0;
card owner

Conclusion: While querying partitions of a range, if you truncate non-overlapping partitions, there will be no locks.
On the other hand, if you truncate the specified partition first, the query will not block as long as it does not query the partition.

The impact of modifying the table space of a partitioned table on table access

session1:
postgres=# begin;
BEGIN
postgres=*# alter table t_partition set tablespace pg_default;
ALTER TABLE
--Lock situation
postgres=# select relation::regclass,mode,pid from pg_locks;
  relation | mode | pid
------------- + -------------------------- + -------
             | ExclusiveLock | 65011
 pg_locks | AccessShareLock | 64609
             | ExclusiveLock | 64609
 t_partition | AccessExclusiveLock | 65011
             | ExclusiveLock | 65011
(5 rows)


session2 (access partition through main table):
postgres=# select * from t_partition where p>='2022-3-13' and p<'2022-3-14';
card owner

session3 (direct access to partition):
postgres=# select * from p0;
          p | v1 | v2
--------------------- + ---- + ------
 2022-03-13 00:00:00 | 1 | aaaa
(1 row)
Unblocked

Conclusion: In the case where the partition table modifies the table space, direct access to the partition will not be blocked. Accessing partitions through tables will be blocked.

Partition table modification table space

By default, it is under the pg_default tablespace.
--Insert data into p0 partition
postgres=# insert into t_partition select '2022-3-13','aa' from generate_series(1,100000) as id;
INSERT 0 100000
--Insert data into p1 partition
postgres=# insert into t_partition select '2022-3-14','aa' from generate_series(1,100000) as id;
INSERT 0 100000

1 Modify the table space of the main table and find that there is no occupied space, which means that the table space of the sub table is still pg_default
postgres=# alter table t_partition set tablespace tbs;
ALTER TABLE

[pg13@sdw2 13580]$ pwd
/home/pg13/tbs/pg13/PG_13_202007201/13580
[pg13@sdw2 13580]$ du -sh
0.

2 Modify the p0 table space and find that the data of p0 has reached the specified table space.
postgres=# alter table t_partition set tablespace tbs;
ALTER TABLE
[pg13@sdw2 13580]$ du -sh
8.5M.

3 Modify the table space of p1 and find that the data of p1 has reached the specified table space.
postgres=# alter table p1 set tablespace tbs;
ALTER TABLE
[pg13@sdw2 13580]$ du -sh
13M .---from 8.5M to 13M


in conclusion:
If you modify the table space of a partitioned table, you need to modify each partition sub-table. Otherwise, only the table space of the main table will be modified and the sub-table will still be in the default table space.

Query the day partition using current_date

select * from t_partition,pg_sleep(100) where p=current_date
In this case, there will be as many accesssharelock locks as there are partitions, which will result in many shared locks.

select * from t_partition,pg_sleep(100) where p=2022-5-11’::date
In this way, accessharelock is only added to the main table and the specified partition table.

Table (partitioned table) and MAX_LOCKS_PER_TRANSACTION

We modify max_connection and MAX_LOCKS_PER_TRANSACTION

postgres=# show max_connections;
 max_connections
------------------
 10
(1 row)

postgres=# show max_locks_per_transaction;
 max_locks_per_transaction
--------------------------
 11
(1 row)

#Create 1000 partition tables
BEGIN;
CREATE TABLE t_part (id int) PARTITION BY LIST (id);
 
SELECT 'CREATE TABLE t_part_' || id
    || ' PARTITION OF t_part FOR VALUES IN ('
    || id || ');'
FROM generate_series(1, 1000) AS id;
 
\gexec
...........
CREATE TABLE
CREATE TABLE
CREATE TABLE
#Error reported during creation
ERROR: out of shared memory
HINT: You might need to increase max_locks_per_transaction.
ERROR: current transaction is aborted, commands ignored until end of transaction block
ERROR: current transaction is aborted, commands ignored until end of transaction block
ERROR: current transaction is aborted, commands ignored until end of transaction block
.............

The number of locks we can keep in shared memory is max_connections x max_locks_per_transaction=110. Remember, row-level locks have nothing to do with this. If it exceeds a certain number, an error will be reported.

Reference: https://www.cybertec-postgresql.com/en/postgresql-you-might-need-to-increase-max_locks_per_transaction/