Transactions and Concurrency
Using BEGIN, COMMIT, and ROLLBACK
In the previous chapter, we learned that when multiple SQL statements need to be treated as a single unit of work, we need to run them inside a transaction.
In this chapter, we'll look at how transactions work in PostgreSQL and learn how to control them using BEGIN, COMMIT, and ROLLBACK.
Implicit transactions
PostgreSQL executes every SQL statement inside a transaction.
For example, the following statement runs inside an implicit transaction:
123UPDATE ordersSET status = 'shipped'WHERE payment_reference = 'pay_00000001';If the statement succeeds, PostgreSQL commits the change. If the statement fails, PostgreSQL doesn't keep any of its changes.
We don't have to explicitly start or end the transaction. PostgreSQL handles that for us.
Explicit transactions
When multiple SQL statements need to be treated as a single unit of work, we can use an explicit transaction.
For example:
1234567BEGIN;
INSERT INTO orders ...;
INSERT INTO order_items ...;
COMMIT;Here, we explicitly tell PostgreSQL where the transaction starts and when its changes should be saved.
We control an explicit transaction using BEGIN, COMMIT, and ROLLBACK.
BEGIN
BEGIN starts an explicit transaction:
1BEGIN;Any statements we execute after BEGIN become part of that transaction until we end it with COMMIT or ROLLBACK.
COMMIT
COMMIT ends the transaction and saves all the changes made during it:
1COMMIT;ROLLBACK
ROLLBACK ends the transaction and discards all the changes made during it:
1ROLLBACK;A successful transaction
Remember the two database operations we ran in the previous chapter?
Create an order in the
orderstable.Add the purchased items to the
order_itemstable.
We previously ran those statements separately. Let's now run them inside an explicit transaction and see what happens when both statements succeed and when one of them fails.
First, let's see what happens when both statements succeed.
Start a transaction:
1BEGIN;You should see BEGIN, which means PostgreSQL has started the transaction.
Now create an order for customer 2:
1234567891011121314INSERT INTO orders ( customer_id, status, total_amount, payment_reference, placed_at)VALUES ( 2, 'pending', 49.99, 'txn_demo_002', CURRENT_TIMESTAMP);You should see INSERT 0 1. The final 1 means PostgreSQL successfully inserted one row into the orders table.
The statement succeeded, but the transaction is still open. We haven't committed its changes yet.
Now add a purchased item to the order_items table:
123456789101112131415INSERT INTO order_items ( order_id, product_id, quantity, unit_price)SELECT id, 1, 1, 49.99FROM ordersWHERE payment_reference = 'txn_demo_002'ORDER BY id DESCLIMIT 1;You should again see INSERT 0 1.
Both statements have succeeded, so we can now save the changes by committing the transaction:
1COMMIT;You should see COMMIT. The transaction is now complete and its changes have been saved.
Let's verify that the order and its order item are both in the database:
123456789SELECT o.payment_reference, oi.product_id, oi.quantity, oi.unit_priceFROM orders AS oJOIN order_items AS oi ON oi.order_id = o.idWHERE o.payment_reference = 'txn_demo_002';You should see the order item associated with txn_demo_002.
Our transaction looked like this:
123456789BEGIN ↓create order → succeeds ↓add order item → succeeds ↓COMMIT ↓both changes are savedBecause both operations succeeded, we used COMMIT to save all the changes made during the transaction.
Now let's see what happens when one of the statements fails.
A failed transaction
Start another transaction:
1BEGIN;You should see BEGIN.
Now create an order for customer 3:
1234567891011121314INSERT INTO orders ( customer_id, status, total_amount, payment_reference, placed_at)VALUES ( 3, 'pending', 49.99, 'txn_demo_003', CURRENT_TIMESTAMP);You should see INSERT 0 1.
The order has been inserted, but the transaction is still open. We haven't committed the change.
Now we'll deliberately make the next statement fail:
123456789101112131415INSERT INTO order_items ( order_id, product_id, quantity, unit_price)SELECT id, 1, 1, 'not-a-price'FROM ordersWHERE payment_reference = 'txn_demo_003'ORDER BY id DESCLIMIT 1;PostgreSQL rejects the statement because unit_price is a numeric column and 'not-a-price' isn't a valid numeric value.
When a statement fails inside an explicit transaction, PostgreSQL marks the transaction as failed. We can't continue using the transaction normally until we roll it back.
The order and its order item were supposed to be created together, but the order item couldn't be created. We therefore don't want to keep the order created by the first statement.
Run:
1ROLLBACK;You should see ROLLBACK. PostgreSQL ends the transaction and discards all the changes made during it.
Now check whether the order still exists:
12345678SELECT id, customer_id, status, total_amount, payment_referenceFROM ordersWHERE payment_reference = 'txn_demo_003';The query should return no rows.
The INSERT into orders succeeded earlier, but it happened inside a transaction that we never committed. When we ran ROLLBACK, PostgreSQL discarded that change.
Our transaction looked like this:
1234567BEGIN ↓create order → succeeds ↓add order item → fails ↓ROLLBACK (the order is discarded)The two SQL statements now share one outcome. If both succeed, we can use COMMIT to save the changes. If one fails, we can use ROLLBACK to discard the changes made during the transaction.