Transactions are the backbone of reliable application data stores. As a full-stack developer, understanding transactional guarantees unlock powerful techniques for building robust web apps.

PostgreSQL has a mature implementation of ACID transactions with performance suitable for high-throughput applications. This comprehensive guide aims to impart a deep working knowledge of PostgreSQL transaction fundamentals for full-stack developers.

Transaction Basics

A database transaction symbolizes a unit of work performed within a database management system (DBMS) against a database, with the following key properties:

Atomicity – Either wholly executed or not executed at all

Consistency – Takes database from one valid state to another

Isolation – Executes independently without interference

Durability – Committed results are permanent

By bundling SQL statements into a transaction, you can ensure database changes either completely succeed or completely fail – no partial updates allowed. This prevents data corruption and ensures integrity.

How Transactions Maintain Database Consistency

Consider an application transferring funds:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; 
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

If the transaction succeeds, 100 units is deducted from Account 1 and added to Account 2 – maintaining system consistency. But if the server crashed after deducting 100 units from Account 1 but before depositing into Account 2, inconsistencies arise.

By atomically linking these SQL statements through a transaction, either all succeed or none, preventing such inconsistencies.

Starting PostgreSQL Transactions

SQL statements after BEGIN become part of a transaction block:

BEGIN;

-- SQL statements here 

COMMIT;

Some ways to begin PostgreSQL transactions:

BEGIN;
START TRANSACTION; 
BEGIN WORK;

The transaction stays open until explicitly terminated with COMMIT or ROLLBACK.

You can verify if you‘re in a transaction block:

SELECT * FROM pg_transaction;

This system catalog table lists details on active backend transactions.

Nested Transactions

Savepoints allow nested sub-transactions within a transaction using SAVEPOINT:

BEGIN;
SAVEPOINT sp1;      -- Nested transaction 1
SAVEPOINT sp2;      -- Nested transaction 2 

ROLLBACK TO sp1;    -- Rollback sp2, but not sp1
COMMIT;

Savepoints enable partial rollbacks within complex transactions – very powerful!

Transaction Isolation Levels

Isolation determines how visible changes in one transaction are to others before commit. Lower isolation risks data inconsistency, while higher isolation impacts performance through locking.

PostgreSQL defaults to READ COMMITTED isolation. Other levels:

Isolation Level Dirty Reads Non-Repeatable Reads Phantoms
Read Uncommitted Allowed Allowed Allowed
Read Committed Not Allowed Allowed Allowed
Repeatable Read Not Allowed Not Allowed Allowed
Serializable Not Allowed Not Allowed Not Allowed

Where:

  • Dirty read – Reading uncommitted data from another transaction
  • Non-repeatable read – Same query yields different row values within a transaction
  • Phantom read – New rows added matching query condition within transaction

For example, set repeatable read isolation:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

Higher isolation prevents more inconsistent scenarios – at a performance impact from increased locking.

Committing Transactions

The COMMIT statement permanently saves transaction changes to the database.

For example:

BEGIN;
DELETE FROM products WHERE expired = true; 
COMMIT;

Committing also ends the current transaction – you would need to reissue BEGIN to start another. Attempting to commit without an active transaction raises an error.

Two-Phase Commit Protocol

For highly available distributed databases, PostgreSQL uses a two-phase commit protocol to finalize transactions:

  1. Coordinator node asks worker nodes to PREPARE TRANSACTION
  2. Workers durably store transaction operations and respond back READY TO COMMIT
  3. If all workers ready, coordinator sends COMMIT TRANSACTION, otherwise ROLLBACK TRANSACTION
  4. Workers accordingly commit or rollback and respond TRANSACTION COMMITTED / ROLLED BACK

This ensures atomic coordination across multiple nodes. If the coordinator fails mid-way, workers deterministically resolve amongst themselves whether to commit or abort based on status.

Below illustrates this:

two-phase-commit

Two-Phase Commit Sequence [Source: Real Python]

So despite failures, transactions maintain ACID durability.

Rolling Back Transactions

The ROLLBACK statement discards all changes from the current transaction and ends it. For example:

BEGIN;

UPDATE employees SET salary = salary * 1.05;

-- Discard entire transaction
ROLLBACK; 

Rolling back resets the database state, undoing any actions from that transaction. Useful for cancelling transactions on errors.

Attempting to rollback without an active transaction raises an exception. Rollbacks can be nested to any depth.

Handling Errors and Exceptions

If any statement inside a transaction raises errors or exceptions, PostgreSQL automatically rolls back the whole transaction:

BEGIN;

-- Division by zero error
UPDATE metrics SET ratio = clicks/pageviews;

SELECT * FROM users; -- Never executed

COMMIT; 

-- Automatically rolled back

So you get all-or-nothing atomicity during errors. For more control over exceptions, use a declarative handler:

DECLARE exit handler FOR SQLSTATE ‘23503‘
BEGIN
  ROLLBACK;
  -- Respond to unique constraint violation 
END;

The exit handler intercepts the error and triggers a rollback.

Transaction Performance

PostgreSQL offers excellent transaction performance suitable for high-traffic web apps. As an open-source database, PostgreSQL competes well against proprietary databases like Oracle.

Some key performance benchmarks on standard hardware:

Transaction Benchmark Transactions per second
Light read-write 19,000 TPS
Heavy read-write 13,000 TPS
Read-only 112,000 TPS
OLTP read-write 21,000 TPS

PostgreSQL achieves efficient transactions via:

Write-Ahead Logging (WAL) – For crash recovery, changes are first written to a redo sequential log, then applied to data files. This guarantees durability.

MVCC – Enables snapshot isolation between transactions through time-stamped database row versions.

Effective Cache Usage – Frequently accessed data is cached in-memory for faster reads.

Cost-Based Optimizer – SQL queries are intelligently optimized for superior performance.

So PostgreSQL offers rock-solid transaction support for application workloads.

Using Transactions in Web Applications

Transactions are vital for building reliable web apps and APIs. Here are some typical use cases:

Data Modifications – Bundling SQL writes into transactions ensures consistency:

# Python + PostgreSQL

def transfer_funds(db, from_id, to_id, amount):
    with db.transaction():  
        db.execute(f"UPDATE accounts SET balance = balance - {amount} WHERE id = {from_id}")  
        db.execute(f"UPDATE accounts SET balance = balance + {amount} WHERE id = {to_id}")

Here the funds transfer succeeds or fails atomically.

Business Process Coordination – Transactions can orchestrate multiple steps in business processes:

with db.transaction():

    order = db.create_order(purchase)
    inventory.decrement(purchase.items)  
    payments.capture(purchase.amount)
    analytics.record(purchase)

# All steps or none commit  

Concurrency Control – Transactions avoid race conditions in multi-user apps:

// JavaScript + PostgreSQL

async function bookAppointment(timeslot) {  

  const client = await pool.connect()

  try {
    await client.query(‘BEGIN‘)   
    let result = await client.query(‘SELECT * FROM appointments WHERE timeslot = $1‘, [timeslot])
    if (result.rowCount > 0) throw ‘Slot taken‘

    await client.query(‘INSERT INTO appointments VALUES ($1)‘, [timeslot]) 
    await client.query(‘COMMIT‘)
  } catch (err) {
    await client.query(‘ROLLBACK‘) 
    throw err
  } finally {
    client.release()
  }

} 

Here transaction blocks manage concurrency for atomic slot booking.

Error Resilience – Transactions enable fault tolerance in distributed systems:

@transactional 
def payment(order):
    # Call 3 microservices
    inventory.decrement(order)   
    accounts.settle(order)
    logistics.deliver(order)

    # Atomically succeeds or rolls back   

The @transactional decorator handles failures behind the scenes.

Best Practices for PostgreSQL Transactions

Here are some tips for maximizing transaction performance, scale, and correctness in PostgreSQL:

  • Keep transactions as short in duration as possible to minimize resource locking. Break up huge transactions.
  • Use savepoints for partial rollbacks instead of error handling queries. Keeps code clean.
  • Profile isolation levels to balance correctness against performance.
  • Set a statement timeout period to avoid long-running transactions blocking writes.
  • Ensure adequate indexes to improve transactional performance for read queries.
  • Follow USE methodology – Unit test code, System integration test, Manual verification before release.
  • Prevent transaction writes exceeding storage limits or table size thresholds.
  • Set up monitoring for transaction metrics like latency, throughput, row counts etc.

Correct transaction usage is key to fault-tolerant application data stores.

Conclusion

PostgreSQL offers a mature ACID-compliant implementation of database transactions – a must-have for reliable application data stores.

As a full-stack developer, thoroughly understanding transaction isolation semantics, performance tradeoffs, and architectural impact will allow building correct web-scale apps.

Harnessing PostgreSQL‘s highly optimized transaction engine can deliver tremendous value for developers building applications of any complexity.

Similar Posts