Advanced database locking and concurrency control

Maria Garcia Feb 2026
2 tabs
-- Row-level locks with SELECT FOR UPDATE
BEGIN;

SELECT * FROM inventory
WHERE product_id = 123
FOR UPDATE;  -- Locks selected rows

-- Other transactions wait here
-- Process and update
UPDATE inventory SET quantity = quantity - 1
WHERE product_id = 123;

COMMIT;

-- NOWAIT: Don't wait for lock
BEGIN;
SELECT * FROM inventory
WHERE product_id = 123
FOR UPDATE NOWAIT;
-- Raises error if locked

-- SKIP LOCKED: Skip locked rows (job queue pattern)
BEGIN;
SELECT * FROM job_queue
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;

-- Multiple workers can process different jobs concurrently
UPDATE job_queue SET status = 'processing' WHERE id = ?;
COMMIT;

-- FOR SHARE: Allows concurrent readers, blocks writers
SELECT * FROM products
WHERE id = 123
FOR SHARE;
-- Other SELECT FOR SHARE allowed
-- UPDATE blocks

-- Table-level locks
BEGIN;
LOCK TABLE products IN ACCESS EXCLUSIVE MODE;
-- No other access allowed

TRUNCATE TABLE products;
COMMIT;

-- Lock modes (least to most restrictive):
-- ACCESS SHARE: SELECT
-- ROW SHARE: SELECT FOR UPDATE
-- ROW EXCLUSIVE: INSERT, UPDATE, DELETE
-- SHARE UPDATE EXCLUSIVE: VACUUM, CREATE INDEX CONCURRENTLY
-- SHARE: CREATE INDEX
-- SHARE ROW EXCLUSIVE: (rare)
-- EXCLUSIVE: (rare)
-- ACCESS EXCLUSIVE: DROP TABLE, TRUNCATE, VACUUM FULL

-- Advisory locks (application-level)
-- Take advisory lock
SELECT pg_advisory_lock(123);

-- Do work exclusively
-- ...

-- Release lock
SELECT pg_advisory_unlock(123);

-- Try lock (non-blocking)
SELECT pg_try_advisory_lock(123);
-- Returns true if acquired, false if already locked

-- Session-level advisory lock
SELECT pg_advisory_lock(12345);
-- Released when session ends

-- Transaction-level advisory lock
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- Released at transaction end
COMMIT;

-- Use advisory locks for distributed coordination
/*
// Node.js example
const lockId = 12345;

// Try to acquire lock
const result = await client.query(
  'SELECT pg_try_advisory_lock($1)',
  [lockId]
);

if (result.rows[0].pg_try_advisory_lock) {
  try {
    // Do exclusive work
    await processJob();
  } finally {
    // Release lock
    await client.query(
      'SELECT pg_advisory_unlock($1)',
      [lockId]
    );
  }
} else {
  console.log('Job already being processed');
}
*/

-- Deadlock detection
SET deadlock_timeout = '1s';

-- Session 1:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Waits for lock on id = 2
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- Session 2 (simultaneously):
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
-- Waits for lock on id = 1 -> DEADLOCK!
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
-- One transaction aborted automatically

-- Prevent deadlocks: Acquire locks in consistent order
BEGIN;
-- Always lock lower ID first
UPDATE accounts SET balance = balance - 100
WHERE id = LEAST(1, 2);

UPDATE accounts SET balance = balance + 100
WHERE id = GREATEST(1, 2);
COMMIT;
2 files · sql Explain with highlit

Database locks ensure data consistency in concurrent access. I understand lock types—row, table, advisory. Shared locks allow concurrent reads. Exclusive locks prevent all access. Understanding lock granularity prevents contention. Deadlocks occur when transactions wait circularly—detection and retry needed. Lock timeouts prevent hung transactions. FOR UPDATE locks rows for modification. SKIP LOCKED enables job queue patterns. Advisory locks provide application-level coordination. Proper locking balances consistency and concurrency. Essential for high-traffic applications, background jobs. PostgreSQL's MVCC minimizes locking overhead.