25.2 FOR SHARE, FOR NO KEY UPDATE, FOR KEY SHARE: Weaker Lock Modes
Alright, let’s pull back the curtain on the weaker lock modes. You’ve met the heavy hitters—FOR UPDATE and its row-level exclusive lock. But sometimes you need to collaborate, not conquer. That’s where this crew comes in. They’re the diplomats of the locking world, allowing varying degrees of shared access while still maintaining some control. They exist because the designers realized that slapping a fully exclusive lock on everything is like using a sledgehammer to crack a walnut—effective, but you’ll make a huge mess and nobody else can have any walnuts.
The Shared Access Spectrum
These locks aren’t a binary choice; they’re a spectrum of “sharing-ness.” The weakest is FOR KEY SHARE, then FOR SHARE, then FOR NO KEY UPDATE, and finally the fully exclusive FOR UPDATE. The strength dictates what other locks can coexist on the same row. A weaker lock will conflict with fewer types of stronger locks. The official documentation has a nifty conflict table, but the real trick is understanding the why.
FOR SHARE: The “Look, Don’t Touch” Lock
This is your classic read lock. SELECT FOR SHARE says, “I’m probably reading this data to make a decision for a later update. You can look at it too, and you can even FOR SHARE it yourself, but nobody gets to change it or put a stronger lock on it until I’m done.”
It’s perfect for situations where you need a consistent view of multiple rows for a business transaction without preventing other transactions from also reading those same rows.
-- Transaction 1
BEGIN;
SELECT * FROM accounts WHERE owner = 'alice' FOR SHARE;
-- I'm now looking at Alice's accounts, maybe to check a total balance...
-- ... [application logic happens] ...
Meanwhile, in another session:
-- Transaction 2
BEGIN;
SELECT * FROM accounts WHERE id = 123 FOR SHARE; -- This works fine.
UPDATE accounts SET balance = balance + 100 WHERE id = 123;
-- This UPDATE will BLOCK, waiting for Transaction 1 to commit or roll back.
The key pitfall here? If you FOR SHARE a million rows, you’re holding a million row-level locks. This can crush performance and potentially lead to lock exhaustion. Use it judiciously.
FOR NO KEY UPDATE: The “I’m Updating, But Not The Key” Lock
This one is clever, and its name is painfully literal. It’s stronger than FOR SHARE but weaker than a full FOR UPDATE. It says, “I am going to update this row, but I promise I won’t update any columns that are part of a foreign key.” Why does that promise matter? Because it allows for a more efficient concurrency trick.
Since you’re not touching the key, other transactions can still use FOR KEY SHARE locks on this same row. And FOR KEY SHARE is what PostgreSQL takes automatically when it checks a foreign key constraint. So, by using FOR NO KEY UPDATE, you avoid blocking those crucial foreign key checks.
-- Transaction 1: We're updating a non-key field on an invoice
BEGIN;
SELECT * FROM invoices WHERE id = 456 FOR NO KEY UPDATE;
-- Let's say we update a 'notes' column...
UPDATE invoices SET notes = 'Paid via wire transfer' WHERE id = 456;
Now, a concurrent transaction that inserts an invoice line item won’t be blocked:
-- Transaction 2: Inserting a line item that references our invoice
BEGIN;
INSERT INTO invoice_lines (invoice_id, description, amount)
VALUES (456, 'Consulting Fee', 500.00); -- This does NOT block!
-- The FK check on invoice_lines.invoice_id uses a FOR KEY SHARE lock,
-- which is compatible with our FOR NO KEY UPDATE lock.
COMMIT;
If Transaction 1 had used a full FOR UPDATE, Transaction 2 would have been blocked waiting for the exclusive lock. This is a huge win for concurrency in parent-child table relationships.
FOR KEY SHARE: The Super Permissive Lock
This is the weakest of the weak. It only conflicts with FOR UPDATE and FOR NO KEY UPDATE. It basically says, “I’m only interested in this row’s key existence for a foreign key relationship. Change anything else you want, just don’t delete this row or change its key so my FK becomes invalid.”
You’ll rarely use this explicitly. It’s the lock PostgreSQL itself uses to validate foreign keys during an INSERT or UPDATE. Its main utility for you is in understanding why other operations don’t get blocked.
Best Practices and The Gotcha
The biggest “gotcha” isn’t technical, it’s philosophical: you must hold these locks until the end of your transaction. The moment you COMMIT or ROLLBACK, the locks are released. The entire point is to create a stable view of the data for the duration of your business logic. Releasing the lock early defeats the purpose.
The best practice? Keep your transactions as short as possible. Do your application logic before you acquire the lock. Get your data, make your decisions, open the transaction, acquire the lock, do the write, and commit immediately. Holding any lock, even a weak one, across network calls or user input is a recipe for a slow, deadlock-ridden application. Don’t be that person. Your brilliant friend wouldn’t.