Menu
Try in Playground

SQLite Window Functions: OVER, PARTITION BY, and Frames

How window functions work in SQLite — OVER, PARTITION BY, ranking functions, LAG/LEAD, and frame clauses for running totals.

This page includes runnable editors — edit, run, and see output instantly.

A Window Function Adds a Column Without Collapsing Rows

GROUP BY reduces many rows into one. A window function does something different: it computes a value across a set of related rows but keeps every input row in the output. You get the row-by-row detail and the aggregate, side by side.

The shape is always the same: a function, then OVER (...).

The total_all column shows the grand total of every row, repeated on each line. The original rows are untouched. Compare that to SELECT SUM(amount) FROM sales — same number, but only one row comes back. Window functions give you both views at once.

PARTITION BY: Aggregate Within Groups

An empty OVER () aggregates over the whole table. Add PARTITION BY to aggregate within groups, much like GROUP BY — but again, without collapsing rows.

Each row gets its region's total and its share of that total. With a plain GROUP BY you'd lose the per-employee detail. That's the headline win of window functions: detail and aggregate in one query.

Ranking: ROW_NUMBER, RANK, DENSE_RANK

The ranking family numbers rows according to an ORDER BY inside OVER. The three flavors differ in how they handle ties.

Reading the output:

  • ROW_NUMBER() is always unique — ties get broken arbitrarily. Use it when you need a stable, distinct number per row.
  • RANK() gives tied rows the same rank, then skips the next numbers. Two players tied at 1 are followed by rank 3.
  • DENSE_RANK() also ties, but doesn't skip. The next rank is 2.

For "top N per group," combine ranking with PARTITION BY and filter in an outer query — WHERE can't reference window functions directly:

Top two earners per region.

LAG and LEAD: Look at Neighboring Rows

LAG(col) returns the value of col from the previous row in the window. LEAD(col) reaches forward. Both are perfect for change-over-time questions.

The first row's yesterday is NULL — there's nothing before it. You can supply a default: LAG(celsius, 1, celsius) OVER (ORDER BY day) would use today's value when no previous row exists.

LEAD is the mirror image. Combine the two with PARTITION BY to get per-group sequences, like comparing this month's sales to the previous month within each region.

Running Totals With Window Frames

Add ORDER BY inside OVER, and aggregate functions like SUM, AVG, COUNT start computing cumulatively:

Two things to notice:

  • SUM(amount) OVER (ORDER BY day) is a running total. The default frame when you write ORDER BY without an explicit frame is "from the start of the window through the current row."
  • The second column uses an explicit frame: ROWS BETWEEN 2 PRECEDING AND CURRENT ROW. That's a 3-row sliding window — a moving average.

The mental model for frames: every window function evaluates over a frame of rows, defined relative to the current row. Common frames:

  • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — running total (the implicit default).
  • ROWS BETWEEN N PRECEDING AND CURRENT ROW — trailing window.
  • ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING — the whole partition.

ROWS counts physical rows. There's also RANGE, which groups by value — handy when you have ties in the ORDER BY column and want them treated as a single step.

FIRST_VALUE, LAST_VALUE, NTILE

A few more window functions worth knowing:

  • FIRST_VALUE and LAST_VALUE return the first or last value within the frame. With LAST_VALUE, watch the frame: the default frame ends at CURRENT ROW, so you usually want ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to get the actual last value of the partition.
  • NTILE(n) splits the rows into n roughly equal buckets — useful for quartiles, percentiles, A/B-style splits.

Naming a Window With WINDOW

When several columns share the same OVER (...) clause, the repetition gets tedious. SQLite lets you name a window once and reuse it:

Same query, less noise. The WINDOW clause goes after WHERE/GROUP BY/HAVING and before ORDER BY.

Window Functions vs GROUP BY

Both involve aggregation, but they answer different questions:

  • GROUP BY reduces. One row per group. Use it when you only want the summary.
  • Window functions preserve. Every input row survives, with extra computed columns alongside.

If you ever find yourself doing a GROUP BY and then re-joining the aggregates back to the original table, that's a strong signal a window function would do the job in one query.

A Couple of Pitfalls

  • WHERE can't reference window functions. Filters happen before windows are computed. Wrap the query in a subquery or CTE and filter on the outer level.
  • Implicit frames bite. SUM(x) OVER (ORDER BY y) is a running total because the default frame is RANGE UNBOUNDED PRECEDING. If you wanted the full-partition sum, write OVER (PARTITION BY ...) without an ORDER BY, or specify the frame explicitly.
  • LAST_VALUE surprises everyone the first time. With the default frame ending at the current row, it returns the current value, not the partition's last. Override the frame.
  • Window functions need SQLite 3.25+ (released in 2018). Any reasonably modern install has them, but some embedded environments lag behind.

Next: Generated Columns

Window functions are query-time computation. The next page covers storage-time computation: generated columns, where the column's value is defined by an expression and updated automatically as the underlying data changes.

Frequently Asked Questions

What are window functions in SQLite?

Window functions compute a value across a set of rows related to the current row, without collapsing them like GROUP BY does. You attach an OVER (...) clause to functions like ROW_NUMBER(), RANK(), SUM(), or LAG() to define the window. Each input row stays in the result — you just get an extra computed column.

What's the difference between RANK and DENSE_RANK in SQLite?

Both assign a rank based on ORDER BY, but they handle ties differently. RANK() leaves gaps after ties — two rows tied at rank 1 are followed by rank 3. DENSE_RANK() doesn't — the next row gets rank 2. Use DENSE_RANK() when you want consecutive ranks, RANK() when the gap is meaningful.

How do I compute a running total in SQLite?

Use SUM(column) OVER (ORDER BY ...) with a window frame. By default, an ORDER BY inside OVER uses the frame RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which gives you a running total. Add PARTITION BY to reset the running total per group.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED