---
title: "13 — Transactions, ACID, and Window Functions: Correct Writes, Per-Row Analytics"
uid: transactions-acid-window-functions
tags: ["sql", "begin", "lead", "roadmap:sql", "commit", "lag", "rank", "savepoint", "rollback", "window-functions", "row-number", "acid", "transactions"]
excerpt: "Transactions make a group of writes all-or-nothing; window functions compute across rows without collapsing them. The step where SQL graduates from rows to correctness."
date: 2026-08-13T03:27:36+0000
source: https://www.aveshina.my.id/en/blog/transactions-acid-window-functions
---

The part where SQL graduates from "read and write rows" to "guarantee writes are correct, and compute across rows without flattening them" is also where I used to mush two ideas together. The separation: **a transaction groups writes so they apply all-or-nothing, and a window function computes over a sliding set of rows while keeping every row in the output.** [1][6]

The framing that clicked is that these solve two different "many rows" problems. Transactions solve *correctness across multiple writes* — a transfer debits one account and credits another, and both must happen or neither. Window functions solve *analytics across multiple reads* — for each order, how does its total compare to the previous order's, and what's its rank within its region. Aggregates collapse rows into one summary; window functions keep the rows and add a computed column beside each.

```figure
<svg viewBox="0 0 720 290" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two panels. Left: a transaction begins, runs two writes, and either COMMITS (both persist) or ROLLS BACK (neither persists) — all or nothing. Right: a window frame slides over a column of rows; for the current row it computes RANK (3), LAG (previous value), and LEAD (next value), and every row remains in the output.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- LEFT: transaction -->
    <text x="150" y="28" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">Transaction: all or nothing</text>
    <rect x="40" y="46" width="220" height="180" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="150" y="68" font-size="11" font-family="ui-monospace,monospace" font-weight="700" fill="#1e1b4b" text-anchor="middle">BEGIN</text>
    <g font-size="10" font-family="ui-monospace,monospace" fill="#475569">
      <text x="60" y="92">UPDATE accounts</text>
      <text x="60" y="108">  SET bal = bal - 100</text>
      <text x="60" y="132">UPDATE accounts</text>
      <text x="60" y="148">  SET bal = bal + 100</text>
    </g>

    <rect x="60" y="166" width="80" height="24" rx="4" fill="#16a34a"/>
    <text x="100" y="182" font-size="10" font-weight="700" fill="#ffffff" text-anchor="middle">COMMIT ✓</text>
    <rect x="160" y="166" width="80" height="24" rx="4" fill="#dc2626"/>
    <text x="200" y="182" font-size="10" font-weight="700" fill="#ffffff" text-anchor="middle">ROLLBACK ↺</text>
    <text x="150" y="210" font-size="9" fill="#64748b" text-anchor="middle" font-style="italic">both writes persist — or neither does</text>

    <!-- RIGHT: window function -->
    <text x="550" y="28" font-size="12" font-weight="700" fill="#134e4a" text-anchor="middle">Window: keep rows, add a calc</text>
    <rect x="430" y="46" width="250" height="200" rx="8" fill="#ccfbf1" stroke="#0d9488" stroke-width="1.5"/>

    <!-- frame -->
    <rect x="460" y="64" width="190" height="40" rx="4" fill="#5eead4" opacity="0.35" stroke="#0d9488" stroke-dasharray="3,3"/>
    <text x="555" y="60" font-size="9" fill="#0d9488" text-anchor="middle" font-style="italic">window frame</text>

    <g font-size="10" font-family="ui-monospace,monospace" fill="#475569">
      <text x="470" y="80">row 1</text>
      <text x="540" y="80">total 30</text>
      <text x="470" y="100">row 2 (current)</text>
      <text x="540" y="100">total 50</text>
      <text x="470" y="120">row 3</text>
      <text x="540" y="120">total 40</text>
    </g>

    <g font-size="9" font-family="ui-monospace,monospace" fill="#134e4a">
      <text x="470" y="148">RANK()    → 3</text>
      <text x="470" y="164">LAG(total) → 30  (prev)</text>
      <text x="470" y="180">LEAD(total)→ 40  (next)</text>
    </g>
    <text x="555" y="206" font-size="9" fill="#64748b" text-anchor="middle" font-style="italic">rows remain — calc added per row</text>
  </g>
</svg>
```

## Transactions and ACID

A **transaction** groups one or more operations into a single unit of work [1]. Its defining property is **ACID** [2][3]:

- **Atomicity** — all operations in the transaction happen, or none do. A failure mid-transaction rolls back everything.
- **Consistency** — a transaction moves the database from one valid state to another, respecting all constraints.
- **Isolation** — concurrent transactions don't see each other's intermediate states (depending on the isolation level).
- **Durability** — once committed, the change survives crashes.

The transfer example is the canonical case: debit one account, credit another. Without a transaction, a crash after the debit leaves money destroyed. Inside BEGIN ... COMMIT, both succeed or both roll back — atomicity guarantees it.

## BEGIN, COMMIT, ROLLBACK, SAVEPOINT

The transaction control verbs [4][5][7]:

- **BEGIN** — starts a transaction. Subsequent statements are part of it until COMMIT or ROLLBACK.
- **COMMIT** — saves all changes made in the transaction, making them permanent and visible to others [5].
- **ROLLBACK** — undoes all changes made since BEGIN, restoring the prior state [4].
- **SAVEPOINT** — a named checkpoint *inside* a transaction. I can ROLLBACK TO savepoint to undo only part of the work without aborting the whole transaction [7].

```
BEGIN;
  UPDATE accounts SET bal = bal - 100 WHERE id = 1;
  SAVEPOINT after_debit;
  UPDATE accounts SET bal = bal + 100 WHERE id = 2;
  -- if something looks wrong:
  ROLLBACK TO after_debit;   -- undoes only the credit
COMMIT;
```

SAVEPOINT is the tool for long transactions where I want to recover from a mid-step error without throwing away everything before it.

## Window functions — analytics without collapsing

This is the part I had to take slowly. A **window function** performs a calculation across a set of rows *related to the current row*, and crucially, it keeps every row in the output — it adds a computed column rather than collapsing rows the way GROUP BY does [6][8].

The syntax has two parts: the function, and an OVER (...) clause that defines the "window" — which rows, in what order:

```
SELECT name, region, total,
       RANK()       OVER (PARTITION BY region ORDER BY total DESC) AS region_rank,
       LAG(total)   OVER (PARTITION BY region ORDER BY placed_at)  AS prev_total,
       SUM(total)   OVER (PARTITION BY region)                    AS region_total
FROM orders;
```

- PARTITION BY region divides rows into buckets (one per region); the function runs within each.
- ORDER BY total DESC defines the ordering the function uses inside each partition.
- The result keeps all original columns and adds the computed column beside each row.

The contrast with aggregates is the whole point: SUM(total) GROUP BY region collapses to one row per region; SUM(total) OVER (PARTITION BY region) keeps every order and appends the region total next to each — so I can compute each order's share of its region in the same query.

## The ranking and offset functions

The window functions the roadmap flags all operate within this OVER (...) frame:

- **ROW_NUMBER()** — a unique sequential integer per row within its partition (1, 2, 3…), no ties [10].
- **RANK()** — ranks with gaps on ties: two rows tied at 1 mean the next is 3 [7].
- **DENSE_RANK()** — ranks without gaps: two rows tied at 1 mean the next is 2 [8].
- **LAG(col) / LEAD(col)** — reach to the previous / next row in the partition without a self-join, for deltas and trend lines [11][12].

```
SELECT name, total,
       total - LAG(total) OVER (ORDER BY placed_at) AS delta_from_prev
FROM orders;
```

That single line — "the difference between this order and the previous one" — would otherwise need a self-join. Window functions express it directly.

## How I use this

Two habits. For transactions: any write that must stay consistent with another write goes inside BEGIN ... COMMIT, and I reach for SAVEPOINT inside long transactions to recover from a recoverable step without aborting the whole thing. For window functions: when I catch myself about to self-join a table to itself to compare a row to its neighbor, or to compute a running total, I stop and write a window function instead — LAG/LEAD for offsets, SUM() OVER (...) for running totals, RANK()/ROW_NUMBER() for "top N per group." They read more clearly and the optimizer handles them better than the self-join they replace.

## References

[1] TutorialsPoint, "SQL Transactions," tutorialspoint.com, 2024. [Online]. Available: [https://www.tutorialspoint.com/sql/sql-transactions.htm](https://www.tutorialspoint.com/sql/sql-transactions.htm)

[2] MongoDB, "A Guide to ACID Properties in Database Management Systems," mongodb.com, 2024. [Online]. Available: [https://www.mongodb.com/resources/basics/databases/acid-transactions](https://www.mongodb.com/resources/basics/databases/acid-transactions)

[3] YouTube, "ACID Explained: Atomic, Consistent, Isolated & Durable," 2023. [Online]. Available: [https://www.youtube.com/watch?v=yaQ5YMWkxq4](https://www.youtube.com/watch?v=yaQ5YMWkxq4)

[4] DigitalOcean, "SQL COMMIT and ROLLBACK," digitalocean.com, 2024. [Online]. Available: [https://www.digitalocean.com/community/tutorials/sql-commit-sql-rollback](https://www.digitalocean.com/community/tutorials/sql-commit-sql-rollback)

[5] Byjus, "Difference between COMMIT and ROLLBACK in SQL," byjus.com, 2024. [Online]. Available: [https://byjus.com/gate/difference-between-commit-and-rollback-in-sql/](https://byjus.com/gate/difference-between-commit-and-rollback-in-sql/)

[6] Mode Analytics, "SQL Window Functions," mode.com, 2024. [Online]. Available: [https://mode.com/sql-tutorial/sql-window-functions](https://mode.com/sql-tutorial/sql-window-functions)

[7] SQLShack, "Overview of SQL RANK Functions," sqlshack.com, 2024. [Online]. Available: [https://www.sqlshack.com/overview-of-sql-rank-functions/](https://www.sqlshack.com/overview-of-sql-rank-functions)

[8] SQLTutorial, "SQL DENSE_RANK," sqltutorial.org, 2024. [Online]. Available: [https://www.sqltutorial.org/sql-window-functions/sql-dense_rank/](https://www.sqltutorial.org/sql-window-functions/sql-dense_rank/)

[9] YouTube, "SQL Window Functions in 10 Minutes," 2023. [Online]. Available: [https://www.youtube.com/watch?v=y1KCM8vbYe4](https://www.youtube.com/watch?v=y1KCM8vbYe4)

[10] SQLTutorial, "SQL ROW_NUMBER," sqltutorial.org, 2024. [Online]. Available: [https://www.sqltutorial.org/sql-window-functions/sql-row_number/](https://www.sqltutorial.org/sql-window-functions/sql-row_number/)

[11] DataCamp, "Understanding the LAG function in SQL," datacamp.com, 2024. [Online]. Available: [https://www.datacamp.com/tutorial/sql-lag](https://www.datacamp.com/tutorial/sql-lag)

[12] Codecademy, "SQL LEAD," codecademy.com, 2024. [Online]. Available: [https://www.codecademy.com/resources/docs/sql/window-functions/lead](https://www.codecademy.com/resources/docs/sql/window-functions/lead)

```quiz
Q: What does the A in ACID guarantee for a transaction?
- Atomicity: every operation in the transaction happens, or none do
- Availability: the database is always reachable
correct: 0
explain: Atomicity means the transaction is indivisible — if any part fails, the whole thing rolls back, so partial writes never persist.

Q: What does ROLLBACK do?
- Undoes all changes made since BEGIN (or back to a SAVEPOINT)
- Saves all changes permanently
correct: 0
explain: ROLLBACK reverts the transaction's changes, restoring the prior state. COMMIT is what persists them.

Q: What distinguishes a window function from an aggregate with GROUP BY?
- A window function collapses rows to one per group
- A window function keeps every row and adds a computed column
correct: 1
explain: Aggregates with GROUP BY collapse rows into summaries. Window functions compute over a frame of related rows but keep every original row in the output.

Q: Two rows tie for rank 1. What rank does the next row get under RANK() vs DENSE_RANK()?
- RANK() gives 3, DENSE_RANK() gives 2
- Both give 2
correct: 0
explain: RANK skips on ties (1, 1, 3); DENSE_RANK does not (1, 1, 2). ROW_NUMBER would give 1, 2, 3 with no ties.

Q: You want each order's difference from the previous order's total. The clean tool is…
- LAG(total) OVER (ORDER BY placed_at)
- a self-join with GROUP BY
correct: 0
explain: LAG reaches to the previous row in the ordered partition without a self-join, expressing the delta directly. Self-joins work but read worse and often optimize worse.
```
