When working with relational databases like PostgreSQL, transactions play a crucial role in ensuring data consistency, reliability, and integrity. Whether you’re building a financial application or managing an e-commerce backend, understanding how PostgreSQL transactions work is essential for any backend developer or database administrator.
In this article, we’ll dive deep into what transactions are, how they work in PostgreSQL, and best practices to use them effectively.
What is a Transaction in PostgreSQL?
A transaction is a sequence of one or more SQL operations executed as a single unit of work. In PostgreSQL, transactions allow you to bundle multiple operations together and ensure they either all succeed or all fail—maintaining data integrity.
PostgreSQL follows the ACID properties to guarantee transaction reliability:
- Atomicity: All operations in a transaction complete successfully or none at all.
- Consistency: The database moves from one valid state to another.
- Isolation: Transactions are isolated from each other.
- Durability: Once a transaction is committed, it is permanently saved.
Basic Syntax of a Transaction
BEGIN;
-- SQL statements
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
In this example:
BEGIN
starts the transaction.- Two
UPDATE
statements transfer money between accounts. COMMIT
saves the changes permanently.
If any statement fails, you can use ROLLBACK
to undo all changes:
BEGIN;
-- Problematic statement
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Suppose this fails
UPDATE non_existing_table SET dummy = 1;
ROLLBACK;
Using Transactions in PostgreSQL via psql
When using the PostgreSQL command-line tool psql
, you can manage transactions interactively:
psql -U username -d database_name
Then:
BEGIN;
-- your queries
COMMIT;
Note: You can also use \x
in psql to enable expanded output and better visualize results during a transaction.
Savepoints in PostgreSQL
PostgreSQL supports savepoints, which allow you to roll back to a specific point within a transaction without rolling back the entire transaction.
BEGIN;
INSERT INTO orders (id, status) VALUES (1, 'pending');
SAVEPOINT save1;
INSERT INTO payments (order_id, amount) VALUES (1, 100);
ROLLBACK TO SAVEPOINT save1;
COMMIT;
This feature is particularly useful when you’re executing complex logic and want more granular control over the rollback process.
Transaction Isolation Levels
PostgreSQL supports four standard SQL isolation levels:
Serializable – Highest level, fully ACID-compliant but can affect performance.
Read Uncommitted – Not supported (defaults to Read Committed).
Read Committed (default) – Sees only committed changes.
Repeatable Read – Prevents non-repeatable reads.
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- statements
COMMIT;
Autocommit Behavior
By default, PostgreSQL operates in autocommit mode, meaning each SQL statement is executed in its own transaction unless you explicitly begin a transaction block.
To disable autocommit in psql
:
\set AUTOCOMMIT off
Or, in a PostgreSQL client or programming language, wrap statements in BEGIN
and COMMIT
.
Transactions in Application Code
Python (using psycopg2):
import psycopg2
conn = psycopg2.connect(database="test", user="user", password="pass")
cur = conn.cursor()
try:
cur.execute("BEGIN")
cur.execute("UPDATE users SET balance = balance - 50 WHERE id = 1")
cur.execute("UPDATE users SET balance = balance + 50 WHERE id = 2")
conn.commit()
except Exception as e:
conn.rollback()
print("Transaction failed:", e)
finally:
cur.close()
conn.close()
Golang:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
func main() {
connStr := "host=localhost port=5432 user=youruser password=yourpassword dbname=yourdb sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
fromUserID := 1
toUserID := 2
amount := 100.0
_, err = tx.Exec("UPDATE users SET balance = balance - $1 WHERE id = $2", amount, fromUserID)
if err != nil {
tx.Rollback()
log.Fatal("Failed to deduct balance:", err)
}
_, err = tx.Exec("UPDATE users SET balance = balance + $1 WHERE id = $2", amount, toUserID)
if err != nil {
tx.Rollback()
log.Fatal("Failed to add balance:", err)
}
err = tx.Commit()
if err != nil {
log.Fatal("Failed to commit transaction:", err)
}
fmt.Println("Transaction completed successfully.")
}
Best Practices
Always use transactions for multiple related write operations.
Use savepoints when running nested or complex logic.
Handle exceptions and always rollback on failure.
Choose the right isolation level based on performance and consistency needs.
Avoid long-running transactions to reduce lock contention.
Common Mistakes to Avoid
Forgetting to COMMIT
or ROLLBACK
a transaction.
Running DDL (e.g., CREATE
, DROP
) inside transactions unintentionally.
Holding transactions open during user input or I/O operations.
Assuming autocommit behaves the same across different environments.
Conclusion
PostgreSQL transactions are powerful tools for ensuring the reliability and consistency of your database operations. Whether you’re performing a simple update or managing thousands of records in complex workflows, mastering transactions is crucial for writing safe and effective SQL.
By following the practices outlined in this guide, you can write robust applications that leverage PostgreSQL’s transactional capabilities to the fullest.
Leave a Reply