SQL window frames · ROWS and RANGE

Window Frames in SQL: ROWS, RANGE, and Peers

Window frames in SQL are the rows a frame-sensitive function can see for the current row. Set ROWS or RANGE so a running total does not jump across tied dates. Ranking, LAG, and LEAD on this page follow partition and order.

Updated September 27, 202623 min readInfiniSynapse Editorial Team
Window Frames in SQL: ROWS, RANGE, and Peers — one rowset splits into ranking, a cumulative frame, and a previous-next comparison
On this page

What are window frames in SQL?

Window frames in SQL are the subset of a partition that a frame-sensitive function can see for the current row. The frame moves as each row is processed. PARTITION BY restarts the calculation; that restart boundary is a partition. When ORDER BY is present and no frame is written, the usual default is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. When ORDER BY is absent, the frame is the whole partition.

SQL window functions still return a value on every detail row through OVER. Ranking, running totals, moving averages, and previous or next comparisons stay on this page. SUM, AVG, FIRST_VALUE, and LAST_VALUE read the frame. ROW_NUMBER, RANK, LAG, and LEAD follow the partition and the order.

A window definition can contain PARTITION BY, window ORDER BY, and a frame. Partitioning decides where calculations restart. Ordering defines sequence and peers. The frame decides which rows around the current row are visible to frame-sensitive calculations. Treat these as separate design choices.

Core window pattern
window_function(expression) OVER (
  PARTITION BY group_key
  ORDER BY sequence_key, unique_tie_breaker
  ROWS BETWEEN frame_start AND frame_end
)

Choose the right window function family

Window functions solve different questions. Ranking functions describe position; offset functions inspect another row; value functions read values at frame boundaries; aggregate functions calculate over a window rather than collapsing a group. Choosing by question prevents complex expressions from being used where a simpler group or join is clearer. ROW_NUMBER, RANK, and LAG use the partition and the order. Frame bounds apply to SUM, AVG, FIRST_VALUE, and LAST_VALUE.

FamilyExamplesQuestion answeredMain caution
RankingROW_NUMBER, RANK, DENSE_RANK, NTILEWhere does this row sit in its partition?Tie semantics and deterministic order
OffsetsLAG, LEADWhat came before or comes next?Sequence gaps and repeated timestamps
Frame valuesFIRST_VALUE, LAST_VALUE, NTH_VALUEWhat value is at a frame boundary?Default frames can surprise LAST_VALUE
Window aggregatesSUM, AVG, COUNT, MIN, MAXWhat is the partition, running, or moving metric?Frame and duplicate-row effects

Separate partition boundaries from row ordering

PARTITION BY customer_id creates an independent customer calculation. Omitting it makes the whole filtered result one partition. Window ORDER BY defines logical order inside each partition but does not guarantee final presentation order. Add a query-level ORDER BY when the delivered rows must be sorted.

Customer order sequence
SELECT
  order_id, customer_id, order_date, amount,
  ROW_NUMBER() OVER (
    PARTITION BY customer_id
    ORDER BY order_date, order_id
  ) AS order_sequence
FROM orders
WHERE status = 'completed'
ORDER BY customer_id, order_sequence;

The unique order_id resolves equal timestamps. Without a tie-breaker, functions that need a unique sequence may choose peer order unpredictably. Peer-aware functions such as RANK intentionally treat equal ordering values together, so the business meaning of ties should determine the function.

Define window frames instead of trusting defaults

A frame is the subset of the partition visible to the current row for frame-sensitive functions. Common units are ROWS, RANGE, and, where supported, GROUPS. ROWS counts physical result rows. RANGE considers ordering values and peers that share that value. GROUPS counts peer groups. Dialect support and restrictions vary.

BoundWhat it reaches
UNBOUNDED PRECEDINGThe first row of the partition
N PRECEDINGN rows before the current row for ROWS, or N units of the sort value for RANGE
CURRENT ROWThe current row; under RANGE, its peer group
M FOLLOWINGM rows or M sort-value units after the current row
UNBOUNDED FOLLOWINGThe last row of the partition
Short formSame as
UNBOUNDED PRECEDINGBETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
N PRECEDINGBETWEEN N PRECEDING AND CURRENT ROW
CURRENT ROWBETWEEN CURRENT ROW AND CURRENT ROW
N FOLLOWINGBETWEEN CURRENT ROW AND N FOLLOWING
UNBOUNDED FOLLOWINGBETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
Deterministic running total
SUM(amount) OVER (
  PARTITION BY customer_id
  ORDER BY order_date, order_id
  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_revenue

With duplicate ordering values, the default RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW includes every peer and can make a running total jump by several rows. Explicit ROWS plus a unique ordering key creates row-by-row progression. For a moving seven-row average, use a bounded row frame; for seven calendar days, row count is not equivalent to time and a supported interval range or time-spine design may be required.

order_idorder_dateamountROWS running sumRANGE running sum
12026-03-01101030
22026-03-01203030
32026-03-02306060

The ROWS column orders by order_date, order_id, so each physical row advances the sum. The RANGE column orders by order_date alone, so both March 1 rows are peers and the first row already shows 30. This is a worked example of peer membership, not a benchmark.

SQL Server accepts RANGE with UNBOUNDED and CURRENT ROW. A count such as 1 PRECEDING belongs on ROWS there. PostgreSQL also accepts GROUPS, which steps by peer group. Confirm the frame your deployed engine actually runs. A year-wide MAX needs ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING; the default upper bound stops at the current row's peers.

LAST_VALUE is a classic default-frame trap. With an ordered default frame ending at the current row's peer group, “last” may mean the current value, not the final value in the partition. Specify an unbounded-following frame when the business question needs the partition's final row.

Apply window functions to real analytical patterns

Top N per group

Rank inside each partition, then filter outside. Choose ROW_NUMBER for exactly N rows or RANK/DENSE_RANK when ties should share position.

Latest row per entity

Order descending by effective timestamp and a unique ingestion key, then keep row number 1. Verify that latest matches the business rule.

Period-over-period change

Use LAG after defining a complete period series. Missing months can otherwise turn “previous row” into an earlier nonadjacent period.

Share of total

Divide a row measure by SUM over its partition, with explicit zero-denominator and duplicate-row handling.

Share and previous-period change
SELECT
  region_id, month_start, revenue,
  revenue / NULLIF(
    SUM(revenue) OVER (
      PARTITION BY month_start
    ), 0
  ) AS share_of_month,
  revenue - LAG(revenue) OVER (
    PARTITION BY region_id
    ORDER BY month_start
  ) AS change_from_prior_month
FROM region_monthly_revenue;

Filter window results at the correct query level

Window functions are logically evaluated after the same-level WHERE, grouping, aggregation, and HAVING. Most dialects therefore cannot reference a window alias in that same level's WHERE. Compute the value in a subquery or CTE, then filter outside. Platforms supporting QUALIFY offer a direct window-result filter.

Top two products per category
WITH ranked_products AS (
  SELECT
    category_id, product_id, revenue,
    ROW_NUMBER() OVER (
      PARTITION BY category_id
      ORDER BY revenue DESC, product_id
    ) AS rn
  FROM product_revenue
)
SELECT *
FROM ranked_products
WHERE rn <= 2;

Filter placement changes the population. Filtering inactive products inside the CTE ranks only active products. Ranking all products and filtering activity outside asks whether the already-ranked winner happens to be active. Both are valid SQL; they answer different questions.

Reduce repeated work across multiple windows

Several functions with the same partition and order can often share a named window or compatible sort. Functions using different ordering keys may require additional sort passes. A query ranking by revenue, sequencing by time, and calculating a moving average by another timestamp may therefore perform several expensive ordered operations even though each expression is short.

Reusable named window
SELECT
  account_id, event_time, amount,
  SUM(amount) OVER w AS running_amount,
  LAG(amount) OVER w AS previous_amount,
  ROW_NUMBER() OVER w AS event_number
FROM account_events
WINDOW w AS (
  PARTITION BY account_id
  ORDER BY event_time, event_id
);

Named windows improve consistency and reduce copy-paste drift, but do not guarantee one physical sort. Inspect the actual plan. Also narrow rows before large sorts when semantics allow; carrying wide text or JSON columns through window operations increases memory and spill risk.

Review window-function performance with evidence

Performance depends on filtered row volume, partition distribution, sorting keys, existing order, memory, parallelism, indexes, and engine behavior. One huge partition can dominate memory even when average partitions are small. Nonselective joins before the window can multiply rows and make every later sort more expensive.

SignalRiskReview
Several distinct window ordersRepeated sorts and memory pressurePlan sort nodes and compatible definitions
Skewed partition keyOne group dominates workLargest and percentile partition sizes
Wide rowsHigher sort and spill payloadEarly projection and late attribute joins
Unbounded framesLarge state or full-partition workConfirm the business really needs all rows

The PostgreSQL window-function reference documents ranking, offsets, value functions, peer groups, and frame-sensitive behavior. Use official documentation for the deployed engine because frame features and null-handling options vary.

Validate window logic before production

  1. Define the input grain. Audit joins and filters before window evaluation.
  2. State the partition. Explain why calculation restarts at that business boundary.
  3. Make order complete. Add stable tie-breakers where row sequence matters.
  4. Specify the frame. Test peers, first rows, last rows, sparse time periods, and empty frames.
  5. Check filter placement. Decide whether eligibility applies before or after the window result.
  6. Compare functions. Test ROW_NUMBER, RANK, and DENSE_RANK at tie boundaries when selecting top N.
  7. Profile partitions. Measure largest groups, not only averages.
  8. Inspect the plan. Review sorts, spills, repeated passes, estimates, and actual rows.

Review window functions as a complete analytical system in production

Production review should follow the data from source grain to final consumer rather than examining an isolated OVER clause. Record which rows enter each partition, which columns make ordering deterministic, which frame boundaries apply, and whether later joins or filters can duplicate or remove ranked rows. A technically valid window can still answer the wrong question when upstream data contains repeated events, late-arriving corrections, or multiple current records.

Build a small adversarial fixture before testing at scale. Include two rows tied on the visible sort key, a null ordering value, a one-row partition, a partition with duplicate timestamps, and a correction arriving after the original event. For running totals, add negative adjustments and duplicate business dates. For LAG or LEAD, verify the first and last rows explicitly. For ranking, decide whether ties should share a rank, create gaps, or be broken by a stable identifier.

Failure signalLikely mechanismEvidence to collect
Rank changes between runsIncomplete tie-breakerRows sharing the complete ORDER BY tuple
Running total jumps unexpectedlyPeer rows or duplicate factsFrame membership and source-key counts
Query sorts repeatedlyIncompatible window specificationsNamed windows and actual plan operators

Finally, reconcile a sample partition manually and preserve that fixture as a regression test. Compare row counts before and after every join, check that the selected “latest” row is unique, and inspect actual memory, sort, spill, and parallelism evidence. Optimization is successful only when the result remains identical for ordinary rows, ties, nulls, sparse partitions, and late corrections.

Document engine-specific behavior too. Default frames, null ordering, support for named windows, and availability of QUALIFY differ across systems. Keep portable business logic separate from dialect shortcuts, and link each shortcut to a tested fallback. This makes migrations and mixed-engine reviews safer without forcing every query into the least capable syntax.

Inspect the complete window-function query

Paste a sanitized complete statement into the InfiniSynapse SQL Complexity Checker so windows, partitions, joins, CTEs, nested queries, and aggregation layers can be reviewed together. Then use your database plan and test data to validate runtime and semantics.

Open SQL Complexity CheckerRemove credentials, secrets, personal data, and sensitive literal values.

SQL window functions frequently asked questions

What is a window function?

It calculates a value for each result row using related rows defined by OVER while preserving detail rows.

How is it different from GROUP BY?

GROUP BY collapses rows into groups; window functions keep rows and attach group- or frame-aware values.

What do PARTITION BY and ORDER BY do?

PARTITION BY defines independent calculation groups. Window ORDER BY defines logical sequence and peers inside each group.

What are window frames in SQL?

Window frames in SQL are the rows a frame-sensitive function can see for the current row. With ORDER BY and no frame written, the usual default is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which includes peers that share the sort value. ROWS counts physical rows. RANGE uses the ordering value.

Is a partition the same as a window frame?

A partition is the restart boundary set by PARTITION BY. The frame is the subset of that partition visible to the current row. One product's rows are a partition. The frame is whichever of those rows the current calculation can see.

Why are results nondeterministic?

The ordering does not uniquely sequence rows. Add a stable tie-breaker when functions require a unique row order.

Official window-function documentation