Understanding query locking is key to using postgres concurrently.
So let’s look at an example to learn more about how locking works and how to see what’s going on within your database.
Playing in the sandbox
To play around with locks let’s first create a sandbox.
Now let’s open two postgres consoles to play in the sandbox.
For clarification let’s name them Alice and Bob. You can SET the console prompt with:
1
sandbox=#\SETPROMPT1'[Alice] %/%R%# '
Now Alice takes a look for what kind of toys there are.
123456789
[Alice] sandbox=#BEGIN;BEGIN[Alice] sandbox=#SELECT*FROMtoys; id | name | usage----+--------+------- 1 | car | 0 2 | digger | 0 3 | shovel | 0(3 rows)
Note we introduce the statement with BEGIN to open the transaction explicitly and keep it open until we commit or rollback.
If Bob does the same he would get the same immediate output.
123456789
[Bob] sandbox=#BEGIN;BEGIN[Bob] sandbox=#SELECT*FROMtoys; id | name | usage----+--------+------- 1 | car | 0 2 | digger | 0 3 | shovel | 0(3 rows)
That is the two SELECT statements do not interfere and can safely run concurrent without blocking each other. Well that’s what we expect from a reliable high performance database.
pg_lock
Nevertheless our transactions are still open. Let’s take a look on a third console, let’s call it Eve, what kind of locks are acquired.
Each active lock is stored in pg_catalogs.pg_lock view. To make sure we only see locks on the sandbox database we join pg_catalog.pg_database and filter bydatname=’sandbox’ OR datname IS NULL`.
To not see locks from our own query we also filter out entries from our own backend_pid. NOT pid = pg_backend_pid();
The relation column is typecast into regclass to make it human readable.
The relationtoys is locked with AccessShareLock by virtual transaction 1/282 from pid 45263 and the lock is granted. That is the transaction got the lock. Everything is fine Bob and Alice are happy as they can both concurrently see what toys are available.
Note that each transaction also hold an `ExclusiveLock on its own virtualxid that is their virtual transaction ID.
Two more locks are added. Alice got a RowExclusiveLock on toys that is the car is now to here. There is also a real transactionid added for Alice on which Alice holds `ExclusiveLock. For every transaction that potentially changes the state of the database a transactionid is added.
MVCC
But as the transaction is not committed yet Bob still sees the old data.
1234567
[Bob] sandbox=#SELECT*FROMtoys; id | name | usage----+--------+------- 1 | car | 0 2 | digger | 0 3 | shovel | 0(3 rows)
As it is not clear whether Alice will commit or rollback her transaction Bob sees the state of the table as it was before Alice started her transaction.
Postgres uses MVCC - Multi Version Concurrency Control to make sure that each transaction always sees a consistent state of the database.
Blocking Queries
Let’s say Bob also wants the car (a pattern that you can see every day in sandboxes):
Now a transactionid for Bob is added and Bob is asking to get a ShareLock on Alice’s transactionid. The typical “mom I wan’t to play with the car too” pattern. As both locks conflict with each other Bob’s request is not granted and he needs to wait until Alice releases her ExclusiveLockon her transactionid by finishing her transaction.
pg_stats_activity
There is another interesting pg_catalog view called pg_stat_activity which shows you what kind of queries are going on:
1234567
[eve] sandbox=#SELECTquery,state,waiting,pidFROMpg_stat_activityWHEREdatname='sandbox'ANDNOT(state='idle'ORpid=pg_backend_pid()); query | state | waiting | pid-----------------------------------------------+---------------------+---------+----- UPDATE toys SET usage = usage+1 WHERE id = 1; | idle in transaction | f | 45263 UPDATE toys SET usage = usage+1 WHERE id = 1; | active | t | 45265(2 rows)
Here we see that Alice’s query is waiting for the transaction to commit while Bob’s query is active and waiting.
Now let’s combine the two to see which query is waiting for whom.
12345678910111213
[eve] sandbox=#SELECTblockeda.pidASblocked_pid,blockeda.queryasblocked_query,blockinga.pidASblocking_pid,blockinga.queryasblocking_queryFROMpg_catalog.pg_locksblockedlJOINpg_stat_activityblockedaONblockedl.pid=blockeda.pidJOINpg_catalog.pg_locksblockinglON(blockingl.transactionid=blockedl.transactionidANDblockedl.pid!=blockingl.pid)JOINpg_stat_activityblockingaONblockingl.pid=blockinga.pidWHERENOTblockedl.grantedANDblockinga.datname='sandbox'; blocked_pid | blocked_query | blocking_pid | blocking_query-------------+-----------------------------------------------+--------------+----------------------------------------------- 45265 | UPDATE toys SET usage = usage+1 WHERE id = 1; | 45263 | UPDATE toys SET usage = usage+1 WHERE id = 1;(1 row)
Here we can clearly see which query is blocked by which statement.
Now if Alice decided to rollback (or commit) her transaction her ExclusiveLock would release and Bob would get the ShareLock on her transactionid.
If Bob now commits his transaction, the row would get UPDATEd depending on whether Alice commited or rolled back.
12
[Alice] sandbox=#ROLLBACK;ROLLBACK
123456789
[Bob] sandbox=#COMMIT;COMMIT[Bob] sandbox=#SELECT*FROMtoys; id | name | usage----+--------+------- 2 | digger | 0 3 | shovel | 0 1 | car | 1(3 rows)
Of course if Bob and Alice would have decided to take different toys no such blocking situation would have appeared.
Explicit Locking
Another typical sandbox pattern is that some kid wants to take it all without really needing it.
Let’s say Alice decides to take the access exclusive.
As Alice is holding AccessExclusiveLock without (yet) changing the database state she has no transactionid and as Bob didn’t get the RowExclusiveLock on the toys table he also doesn’t have any real transactionid. Thus our above query to see blocking statements won’t help (as we join on `transactionid).
Here we can see again whats really going on. By joining the relation on which the lock is hold.
Now Alice has been told that it’s not nice to take an explicit lock without really needing it. So she commits her transaction without doing anything and Bob can get a toy.
12
[Alice] sandbox=#COMMIT;COMMIT
12
UPDATE1[Bob] sandbox=#
Note Bob’s transaction is still open. If we take a look at the locks table we see:
So only after Bob got the RowExclusiveLock a real transactionid for his transaction was added.
So Bob is fine and commits.
12
[Bob] sandbox=#COMMIT;COMMIT
RowExclusiveLock
As Alice is undecided which toy to take and not allowed to take explicit locks she takes another approach.
12345678
[Alice] sandbox=#BEGIN;SELECT*FROMtoysFORUPDATE;BEGIN id | name | usage----+--------+------- 2 | digger | 0 3 | shovel | 0 1 | car | 1(3 rows)
That is “I want to see all toys and maybe I’ll take one but as I’m undecided I don’t want any other to take one until I made my decision.” (Another typical sandbox pattern)
Bob who has decided to take the shovel won’t get it and needs to wait:
Bob who clearly wants to change the database state got an transactionid 24273810 and again has to wait for Alice to get ShareLock on her transactionid 24273809.
Join Locks and Activities
So this time we need our first approach to join locks and activities.
123456789101112131415
[eve] sandbox=#SELECTblockeda.pidASblocked_pid,blockeda.queryasblocked_query,blockinga.pidASblocking_pid,blockinga.queryasblocking_queryFROMpg_catalog.pg_locksblockedlJOINpg_stat_activityblockedaONblockedl.pid=blockeda.pidJOINpg_catalog.pg_locksblockinglON(blockingl.transactionid=blockedl.transactionidANDblockedl.pid!=blockingl.pid)JOINpg_stat_activityblockingaONblockingl.pid=blockinga.pidWHERENOTblockedl.grantedANDblockinga.datname='sandbox'; blocked_pid | blocked_query | blocking_pid | blocking_query-------------+-----------------------------------------------+--------------+-------------------------------- 45265 | UPDATE toys SET usage = usage+1 WHERE id = 2; | 45263 | SELECT * FROM toys FOR UPDATE;(1 row)
So let’s combine both to be 100% sure:
123456789101112131415161718
[eve] sandbox=#SELECTCOALESCE(blockingl.relation::regclass::text,blockingl.locktype)aslocked_item,blockeda.pidASblocked_pid,blockeda.queryasblocked_query,blockedl.modeasblocked_mode,blockinga.pidASblocking_pid,blockinga.queryasblocking_query,blockingl.modeasblocking_modeFROMpg_catalog.pg_locksblockedlJOINpg_stat_activityblockedaONblockedl.pid=blockeda.pidJOINpg_catalog.pg_locksblockinglON(((blockingl.transactionid=blockedl.transactionid)OR(blockingl.relation=blockedl.relationANDblockingl.locktype=blockedl.locktype))ANDblockedl.pid!=blockingl.pid)JOINpg_stat_activityblockingaONblockingl.pid=blockinga.pidWHERENOTblockedl.grantedANDblockinga.datname='sandbox'; locked_item | blocked_pid | blocked_query | blocked_mode | blocking_pid | blocking_query | blocking_mode---------------+-------------+-----------------------------------------------+--------------+--------------+--------------------------------+--------------- transactionid | 45265 | UPDATE toys SET usage = usage+1 WHERE id = 2; | ShareLock | 45263 | SELECT * FROM toys FOR UPDATE; | ExclusiveLock(1 row)
As Eve can’t remember this long query she decides to put that into a view;
Note we added the waiting_duration to see for how long the query was blocked, and added the current_database() function to automatically filter by the database the view belongs to.
Now Eve can simply fire:
12345
[eve] sandbox=#SELECT*fromlock_monitor; locked_item | waiting_duration | blocked_pid | blocked_query | blocked_mode | blocking_pid | blocking_query | blocking_mode---------------+------------------+-------------+-----------------------------------------------+--------------+--------------+--------------------------------+--------------- transactionid | 00:01:02.143922 | 45265 | UPDATE toys SET usage = usage+1 WHERE id = 2; | ShareLock | 45263 | SELECT * FROM toys FOR UPDATE; | ExclusiveLock(1 row)
and is always able to see what the kids are up to. Now Eve decides to get a Latte Macchiato and read the
postgres manual on explicit locking to get an idea of the different lock types and their conflict modes.