FullStack Course LogoFullStack Course
Module: SQL
SQL·190·14 MIN READ

190: Window Functions: PARTITION BY, ORDER BY, Frames, Ranking, and Running Analytics

TOPICS COVERED: Window Functions: PARTITION BY, ORDER BY, Frames, Ranking, and Running Analytics

Learning outcomes

By the end of this lesson, you can:

  • explain and apply window partition in a realistic implementation;
  • explain and apply window ordering in a realistic implementation;
  • explain and apply frames in a realistic implementation;
  • explain and apply ranking functions in a realistic implementation;
  • explain and apply lag and lead in a realistic implementation.

Prerequisites and retrieval

This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before you read, retrieve one concrete example from a previous project where you needed to calculate something across related rows without losing the individual rows. It might have been a customer total, a leaderboard, a previous-event comparison, or a “latest few per group” query. That memory gives the syntax somewhere useful to attach.

The goal is not to memorize a list of window-function names. The goal is to make a defensible decision in a PostgreSQL-backed transactional application, where schema design, correctness, query plans, and concurrency all matter. As you work through the examples, keep asking what the partition is, what establishes the sequence, which rows belong to the frame, and whether ties make the result deterministic.

Terminology

  • Window partition: PARTITION BY divides the input into independent groups while preserving every original row. It is similar to grouping for the purpose of a calculation, but it does not collapse the result to one row per group.
  • Window ordering: An ORDER BY inside OVER establishes the sequence used by ranking, running, lag, and lead calculations. It is separate from the ORDER BY that controls the final display order.
  • Frames: ROWS, RANGE, or GROUPS frame clauses define which rows, peers, or neighboring groups contribute to a calculation relative to the current row. The choice matters when ordering values repeat.
  • Ranking functions: row_number, rank, and dense_rank deal with ties differently. row_number gives every row a distinct position, rank leaves gaps after ties, and dense_rank does not leave those gaps.
  • Lag and lead: lag and lead compare a row with a previous or next row in the window order without requiring a self join. They are useful for change detection, period-over-period metrics, and finding gaps.
  • Top-N per group: Rank rows within each partition, then filter that computed rank in an outer query or CTE. Filtering must happen outside the query level that calculates the window value.

Mental model

Treat Window Functions: PARTITION BY, ORDER BY, Frames, Ranking, and Running Analytics as a design problem with observable inputs, outputs, invariants, and failure modes. A window function looks across related rows and adds a value to each current row; unlike a regular aggregate with GROUP BY, it does not replace those rows with one summary row per group. That is why window functions fit running totals, rankings, comparisons with neighboring rows, and top-N queries so well.

For a useful mental model, imagine the database taking the result set and, for each output row, answering four questions:

  1. Which rows are in this row's partition?
  2. In what order should those rows be considered?
  3. Which portion of that ordered partition is this row's frame?
  4. Which function should be applied to that visible portion or position?

That model is deliberately simplified. PostgreSQL still has to plan and execute the query, and the final result is not guaranteed to be displayed in the window order unless you add a separate outer ORDER BY. A strong implementation makes its assumptions visible, narrows uncertainty at boundaries, and leaves enough evidence—tests, types, constraints, metrics, or diagrams—to prove why the design is safe.

A useful interview and production sequence is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement directly to a library call or a familiar SQL pattern. First state what must remain true. Then choose the partition, order, frame, and function that enforce that contract. For example, “running spend per customer” is incomplete until you decide what happens when two orders have the same ordered_at value.

Deep dive

1. Window partition

Suppose a report needs each order together with the customer's running spend. The calculation must restart for each customer, but the order rows must remain visible. PARTITION BY customer_id creates those independent windows while preserving the rows. Omitting PARTITION BY makes the entire result one partition, so the running total would continue across all customers.

Decision rule: Use window partition deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Confirm that the partition columns match the business boundary: partitioning by branch when the requirement is per customer is syntactically valid but semantically wrong.

2. Window ordering

An ORDER BY inside OVER defines sequence for ranking, running calculations, and neighbor comparisons. It does not sort the rows returned to the client. If the screen or export needs a particular order, use a separate final ORDER BY as well.

Ordering also needs a tie policy. If ordered_at is not unique, two orders can be peers. Add a stable tiebreaker such as an order identifier when the result must be reproducible, and be explicit about NULLS FIRST or NULLS LAST when nulls are possible. This is where a query can appear correct in a small test and still produce confusing results in production.

Decision rule: Use window ordering deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Ask whether the selected order represents business time, insertion order, or merely a display preference.

3. Frames

The window order identifies a sequence, but the frame determines which part of that sequence contributes to the current result. ROWS counts physical rows, while RANGE and GROUPS account for equal ordering values in different ways. The distinction becomes visible when multiple rows share the same timestamp or amount.

For a running total, an explicit frame such as ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW states that the calculation includes every earlier row and the current row. The default frame can surprise last_value queries: with an ordered default frame, “last” is often the last value in the current peer group rather than the last value in the whole partition. If the intended result is the partition-wide last value, specify the frame rather than relying on a default that the next reader may not know.

Decision rule: Use frames deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Test repeated ordering values, not only strictly increasing timestamps.

4. Ranking functions

row_number, rank, and dense_rank are not interchangeable. row_number assigns a unique sequence position even when values tie. rank assigns equal values the same rank and leaves gaps after the tie. dense_rank also assigns equal values the same rank, but the next distinct value receives the next consecutive rank. Choose based on whether positions should be unique and whether tied values should leave gaps.

For a leaderboard, rank may communicate competition-style placement. For “the first three rows” where exactly three rows must be selected, row_number with a deterministic tiebreaker may be the better contract. For “the top three score levels,” dense_rank may be appropriate, although the result can contain more than three rows because of ties.

Decision rule: Use ranking functions deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. State whether “top three” means three rows or three distinct rank values before writing the filter.

5. Lag and lead

lag reads a value from an earlier row in the window order, and lead reads one from a later row. They let you calculate a previous-order delta, detect a change from the previous status, or measure the gap until the next event without writing a self join. The first row has no previous row, and the last row has no next row, so the result is normally NULL unless you provide a default.

The comparison is only meaningful if the order is meaningful. A previous order by timestamp requires a defined tie policy; otherwise, the “previous” row among simultaneous events may not be stable. Keep this in mind when debugging period-over-period numbers that change between executions.

Decision rule: Use lag and lead deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Document whether missing neighbors should remain NULL, use a default, or be excluded from the business metric.

6. Top-N per group

A common requirement is “the top three orders for every branch.” Calculate the rank inside each branch partition, then filter that rank in an outer query or CTE. Window-function results are not available to the WHERE clause at the same query level in which they are calculated, which is why the extra query level is part of the pattern rather than unnecessary ceremony.

Choose the ranking function according to the requirement: use row_number for exactly N rows with a complete deterministic order, or a rank-based function when ties should be retained. A suitable index on the partition and order columns can make this pattern far more efficient, but index usefulness depends on the complete predicate, ordering, data distribution, and query plan. Check with EXPLAIN or EXPLAIN ANALYZE instead of assuming that an index guarantees a particular plan.

Decision rule: Use top-n per group deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Include the tie behavior and expected result size in the query's contract.

Worked example

Consider a PostgreSQL-backed transactional application where schema design, correctness, query plans, and concurrency all matter. Start by writing the requirement in one sentence, list the input and output contracts, and identify which of the concepts above owns each failure mode. The important move is separation: parsing or validation belongs at the boundary; domain rules belong in the domain/service layer; persistence rules belong in the database or repository; presentation rules belong in the client. Mixing these concerns makes a happy-path demo look shorter, but it makes edge cases much harder to reason about.

Here is the running-total portion of that design:

sql
SELECT
  customer_id,
  ordered_at,
  total,
  SUM(total) OVER (
    PARTITION BY customer_id
    ORDER BY ordered_at
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM orders;

Read the query from the inside of the OVER clause outward. Each customer gets a separate partition. Within that partition, rows are considered by ordered_at. The explicit ROWS frame starts at the first row and ends at the current row, so SUM(total) produces a running value rather than one repeated customer total. The final result order is still unspecified; add an outer ORDER BY when consumers need stable presentation order. If two orders can share the same timestamp, decide whether ordered_at, order_id should be the window order and whether the frame should treat peers as one group.

Walk the example with at least four cases: the normal path, an empty or missing value, a duplicate/retry/concurrent path where relevant, and a dependency failure. For each case, state which layer detects the problem and what the caller observes. For example, a missing total raises a data-contract question rather than being solved by the window function; a duplicate order may be a persistence or idempotency issue; a concurrent insert changes what a later query can observe according to the transaction's consistency expectations; and a database outage belongs in dependency error handling. This is the level of explanation expected in a senior code review or technical interview.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. Window queries can be correct and still become expensive when one partition grows very large or when a sort is required. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.

When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations, including what a report is allowed to observe while writes are in progress. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and the network input is untrusted. Parameterize values, enforce authorization outside the query's presentation logic, and do not treat a ranking or filtering query as a security boundary by itself.

For performance work, inspect the actual execution plan and representative data. Look for sorts, scans, row counts, memory pressure, and whether the chosen index helps the full query. A fast query on ten rows says little about a customer with millions of orders or a branch with unusually high activity.

Guided lab

Produce customer running spend, previous-order delta, monthly rank, and top-three orders per branch. Vary the frame definition and explain why last_value can return an unexpected result under a default frame. Include a tie case in the data so that you have to choose between row_number, rank, and dense_rank, and make the ordering deterministic where the requirement calls for reproducible output.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements.
  2. List input, output, and error contracts before implementation.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing. For SQL, inspect representative results and the execution plan where performance is relevant.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note.

Edge cases and failure modes

  • Window partition: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Verify that the calculation restarts at the intended business boundary and does not accidentally use one global partition.
  • Window ordering: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Include ties and null ordering values, and verify both the window sequence and the final output order.
  • Frames: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Compare ROWS, RANGE, or GROUPS when peers exist, especially for running totals and last_value.
  • Ranking functions: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Confirm whether the requirement means exactly N rows or all rows within the top N ranks.
  • Lag and lead: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Check first and last rows, missing values, and tied ordering keys.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong. Start by stating the partition, order, frame, tie policy, and desired result cardinality.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values. In SQL, do not hide ambiguous ordering or null behavior behind an unexplained cast or default.
  • Testing only the happy path and therefore discovering contracts only after integration. Include empty partitions, duplicate ordering values, ties, first/last neighbor rows, and null data.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement. Inspect the real execution plan and data distribution before adding indexes or changing the query.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees. A client can request a different partition or filter, so the server and database must enforce the actual boundary.

For debugging, reproduce the smallest failing case, inspect the actual values and execution plan, trace the boundary where the invariant first becomes false, and fix the owning layer rather than adding a downstream patch. If a running result is wrong, first inspect partition membership, then the window order and tie-breaker, then the frame. If a top-N result has too many or too few rows, inspect the ranking function and the definition of “top.” If lag or lead looks wrong, verify the sequence before investigating arithmetic.

Interview questions

  1. What problem does Window partition solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem does Window ordering solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does Frames solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does Ranking functions solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem does Lag and lead solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Window Functions: PARTITION BY, ORDER BY, Frames, Ranking, and Running Analytics to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code. Be ready to explain what happens when the order contains ties, how the frame changes the result, and why the final display order may need its own ORDER BY.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /sql/lesson/190/window-functions-partition-by-order-by-frames-ranking-and-running-analytics