Transactions
ParticleDB provides full ACID transactions with Multi-Version Concurrency Control (MVCC). Readers never block writers, and writers never block readers.
Quick Start
Section titled “Quick Start”BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Verify the transferSELECT id, name, balance FROM accounts WHERE id IN (1, 2);
COMMIT;If anything goes wrong, roll back all changes:
BEGIN;DELETE FROM important_data WHERE created_at < '2024-01-01';-- Changed my mindROLLBACK;-- Nothing was deletedTransaction Lifecycle
Section titled “Transaction Lifecycle”Start a new transaction. All subsequent statements run within this transaction until COMMIT or ROLLBACK.
BEGIN;-- orBEGIN TRANSACTION;COMMIT
Section titled “COMMIT”Persist all changes made during the transaction. Once committed, changes are durable and visible to other transactions.
COMMIT;-- orCOMMIT TRANSACTION;ROLLBACK
Section titled “ROLLBACK”Discard all changes made during the transaction, restoring the database to its state at BEGIN.
ROLLBACK;-- orROLLBACK TRANSACTION;Auto-Commit
Section titled “Auto-Commit”Without an explicit BEGIN, each statement runs in its own auto-committed transaction:
-- These are two separate transactionsINSERT INTO logs (message) VALUES ('event 1'); -- auto-committedINSERT INTO logs (message) VALUES ('event 2'); -- auto-committedSavepoints
Section titled “Savepoints”Create named checkpoints within a transaction. You can roll back to a savepoint without aborting the entire transaction.
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (1, 100, 250.00);SAVEPOINT after_order;
INSERT INTO order_items (order_id, product, quantity) VALUES (1, 'Widget', 5);-- Oops, wrong productROLLBACK TO SAVEPOINT after_order;
-- The order still exists, but the order_item was rolled backINSERT INTO order_items (order_id, product, quantity) VALUES (1, 'Gadget', 3);
COMMIT;RELEASE SAVEPOINT
Section titled “RELEASE SAVEPOINT”Release a savepoint when it’s no longer needed. This does not commit changes — it simply removes the named checkpoint.
BEGIN;SAVEPOINT sp1;INSERT INTO logs (message) VALUES ('step 1');RELEASE SAVEPOINT sp1;-- sp1 can no longer be rolled back to, but changes are still part of the transactionCOMMIT;Two-Phase Commit (2PC)
Section titled “Two-Phase Commit (2PC)”Two-phase commit enables distributed transactions that span multiple database instances or systems. A coordinator prepares all participants before issuing the final commit.
Phase 1: Prepare
Section titled “Phase 1: Prepare”BEGIN;UPDATE accounts SET balance = balance - 500 WHERE id = 1;PREPARE TRANSACTION 'transfer_tx_001';-- Transaction is now in "prepared" state-- Connection can disconnect; the prepared transaction persistsPhase 2: Commit or Rollback
Section titled “Phase 2: Commit or Rollback”From any connection (including after reconnection):
-- If all participants are ready, commitCOMMIT PREPARED 'transfer_tx_001';
-- If any participant failed, roll backROLLBACK PREPARED 'transfer_tx_001';2PC Guarantees
Section titled “2PC Guarantees”- A prepared transaction survives connection drops and server restarts
- The transaction ID must be unique across all active prepared transactions
- Prepared transactions hold their locks until committed or rolled back
- Use 2PC when coordinating writes across multiple ParticleDB instances or external systems
Isolation and Concurrency
Section titled “Isolation and Concurrency”MVCC (Multi-Version Concurrency Control)
Section titled “MVCC (Multi-Version Concurrency Control)”ParticleDB uses MVCC to provide snapshot isolation:
- Each transaction sees a consistent snapshot of the database as of its start time
- Reads never block writes, and writes never block readers
- Write-write conflicts are detected and one transaction is rolled back
How MVCC Works
Section titled “How MVCC Works”- Read operations see only data committed before the transaction started
- Write operations create new versions of rows
- Concurrent writes to the same row cause a conflict — the second writer is rolled back
- Committed data is visible to all subsequent transactions
-- Transaction A -- Transaction BBEGIN; BEGIN;SELECT balance FROM accountsWHERE id = 1; -- sees 1000 UPDATE accounts SET balance = 900 WHERE id = 1; COMMIT;SELECT balance FROM accountsWHERE id = 1; -- still sees 1000-- (snapshot isolation)COMMIT;-- Now sees 900Write Conflicts
Section titled “Write Conflicts”When two concurrent transactions modify the same row, one will be rolled back:
-- Transaction A -- Transaction BBEGIN; BEGIN;UPDATE accounts UPDATE accountsSET balance = balance - 100 SET balance = balance - 50WHERE id = 1; WHERE id = 1; -- Blocked or rolled backCOMMIT; -- (conflict detected)Row-Level Locking
Section titled “Row-Level Locking”FOR UPDATE
Section titled “FOR UPDATE”Acquire an exclusive lock on selected rows. Other transactions cannot modify or lock these rows until the current transaction completes.
BEGIN;
-- Lock the account row so no other transaction can modify itSELECT * FROM accounts WHERE id = 42 FOR UPDATE;
-- Safe to read and update without race conditionsUPDATE accounts SET balance = balance - 100 WHERE id = 42;
COMMIT;FOR SHARE
Section titled “FOR SHARE”Acquire a shared lock. Multiple transactions can hold FOR SHARE on the same row, but FOR UPDATE is blocked.
BEGIN;
-- Allow concurrent readers but prevent writersSELECT * FROM products WHERE id = 1 FOR SHARE;
-- Read-only operations...SELECT * FROM inventory WHERE product_id = 1;
COMMIT;FOR NO KEY UPDATE / FOR KEY SHARE
Section titled “FOR NO KEY UPDATE / FOR KEY SHARE”Finer-grained lock modes:
-- Lock rows but allow updates that don't modify the keySELECT * FROM orders WHERE id = 1 FOR NO KEY UPDATE;
-- Shared lock that's compatible with NO KEY UPDATESELECT * FROM orders WHERE id = 1 FOR KEY SHARE;NOWAIT
Section titled “NOWAIT”Return an error immediately if the row is already locked, instead of waiting:
BEGIN;SELECT * FROM accounts WHERE id = 42 FOR UPDATE NOWAIT;-- ERROR if another transaction holds a lock on this rowSKIP LOCKED
Section titled “SKIP LOCKED”Skip over rows that are currently locked by other transactions. This is the building block for job queue patterns:
-- Worker picks up the next available jobBEGIN;
SELECT * FROM jobsWHERE status = 'pending'ORDER BY created_atLIMIT 1FOR UPDATE SKIP LOCKED;
-- Process the job...UPDATE jobs SET status = 'processing' WHERE id = <selected_id>;
COMMIT;Locking Specific Tables in Joins
Section titled “Locking Specific Tables in Joins”BEGIN;
SELECT o.id, o.total, c.nameFROM orders oJOIN customers c ON o.customer_id = c.idWHERE o.id = 100FOR UPDATE OF orders;-- Only the orders row is locked; customers row is not
COMMIT;Cursors
Section titled “Cursors”For processing large result sets without loading everything into memory:
BEGIN;
-- Declare a cursorDECLARE order_cursor CURSOR FOR SELECT id, total FROM orders WHERE status = 'pending';
-- Fetch rows in batchesFETCH 100 FROM order_cursor;FETCH 100 FROM order_cursor;
-- Fetch backwardsFETCH BACKWARD 10 FROM order_cursor;
-- Other fetch directionsFETCH FIRST FROM order_cursor;FETCH LAST FROM order_cursor;FETCH ALL FROM order_cursor;
-- Close the cursorCLOSE order_cursor;
COMMIT;Transaction Best Practices
Section titled “Transaction Best Practices”-
Keep transactions short. Long-running transactions hold locks, delay garbage collection, and increase the chance of write conflicts.
-
Use auto-commit for independent reads. Don’t wrap read-only
SELECTstatements in explicit transactions unless you need snapshot consistency across multiple queries. -
Handle conflicts with retry logic. When a write conflict occurs, catch the error and retry the entire transaction:
for attempt in range(3):try:with db.transaction():db.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")db.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")break # successexcept ConflictError:continue # retry -
Use SKIP LOCKED for work queues instead of application-level locking.
-
Prefer savepoints for partial rollback instead of aborting entire complex transactions.
WAL Sync Modes
Section titled “WAL Sync Modes”ParticleDB supports three durability modes, configured at server start:
| Mode | Durability | Performance | Use Case |
|---|---|---|---|
sync | Full (fsync per entry) | Baseline | Production data you can’t lose |
groupsync | Batched (fsync per group) | ~3x faster | Default production mode |
nosync | None (WAL disabled) | ~5x faster | Benchmarks, ephemeral data, caching layers |
# Start with grouped sync (recommended for production)particledb start --wal-sync-mode groupsync
# Start with WAL disabled (maximum throughput)particledb start --wal-sync-mode nosync