Hook: Interviewers love the OVER clause because it tells them whether you can keep each row visible while adding group logic on top — like putting a scoreboard next to every player instead of replacing the team with one number.
Question: What is the OVER clause in SQL?
Answer: The OVER clause turns an aggregate or analytic function into a window function. A window function is a function that looks at a related set of rows, called a window, but still returns one result per original row. That is why it is perfect for running totals, ranks, and per-group calculations without collapsing the result set.
Interview-Ready Answer: I use OVER when I need summary logic and row detail at the same time. It defines the window with optional PARTITION BY, ORDER BY, and sometimes an explicit frame like ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. So I can calculate things like running totals or ranks while still returning every order, customer, or event row.
Detailed Explanation: The OVER clause is the part of a window function that tells SQL which rows this function may see. Think of it as a transparent lens over the data: the rows stay visible, but the function gets a custom view for each row.
FROM, then filters them with WHERE, and applies grouping if you used GROUP BY.OVER(...) defines the window seen by the function.PARTITION BY splits the result into separate buckets, such as one bucket per customer or region.ORDER BY sets the row order inside each bucket, which matters for running totals and ranking.GROUP BY.| Aspect | GROUP BY | OVER |
|---|---|---|
| Row count | Collapses rows | Keeps all rows |
| Main use | One total per group | Per-row analytics |
| Example | Revenue per region | Running revenue per region |
| Output shape | Summary rows | Original rows plus extras |
PARTITION BY is optional grouping inside the window. If you omit it, the function sees the whole filtered result set. ORDER BY is what gives a sequence to rows; without it, ranking and running logic usually make no sense. The frame is where many candidates slip: with ORDER BY present, the default frame in many SQL engines follows the standard idea of RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which means the current row and its peers are included, not necessarily the whole partition.
| Frame type | What it means | Best for |
|---|---|---|
ROWS | Counts physical rows | Running totals |
RANGE | Groups equal sort values | Value-based windows |
That is why ROWS is usually safer for step-by-step running totals, while RANGE can surprise you when two rows share the same sort key, such as the same timestamp or score.
row_number, rank, and dense_rank all depend on ORDER BY.WINDOW clause when several functions share the same partition and order, so you do not repeat yourself.work_mem is too small.PARTITION BY and ORDER BY can reduce or avoid sorting.WHERE and GROUP BY, so you cannot use them directly in WHERE; wrap them in a subquery or CTE.last_value and nth_value are classic gotchas because the default frame often ends at the current row, not the end of the partition.Real-World Example: Imagine a checkout service that builds a merchant dashboard. Each order row needs the merchant's total sales, the order's rank inside that merchant, and the latest order amount for the day. The team uses OVER so the dashboard can show those numbers without losing the original order-level data.
What goes wrong when someone misunderstands it? A developer writes last_value(amount) OVER (PARTITION BY merchant_id ORDER BY created_at) expecting the last order in the merchant's day. The query runs cleanly, but the dashboard quietly shows the current row's amount instead of the partition's true last amount because the default frame stops at the current row. Users see inconsistent totals, finance sees mismatched reports, and logs show no SQL error — only wrong business data.
The fix is to make the frame explicit or to choose a different pattern, such as joining against a max timestamp. That is why window-function bugs are dangerous: they are often semantically wrong, not syntactically broken.
-- PostgreSQL-compatible demo of the OVER clause.
-- It keeps every order row, while adding per-region totals, rankings, and a running total.
WITH sales (sale_id, region, salesperson, sale_date, amount) AS (
VALUES
(1, 'East', 'Ava', DATE '2024-01-01', 100),
(2, 'East', 'Ben', DATE '2024-01-02', 150),
(3, 'East', 'Ava', DATE '2024-01-03', 200),
(4, 'West', 'Noah', DATE '2024-01-01', 80),
(5, 'West', 'Mia', DATE '2024-01-02', 120),
(6, 'West', 'Noah', DATE '2024-01-03', 120)
)
SELECT
sale_id,
region,
salesperson,
sale_date,
amount,
-- OVER () means "use the whole result set".
COUNT(*) OVER () AS total_rows,
-- No ORDER BY here: this is the total for the whole region, repeated on each row.
SUM(amount) OVER (PARTITION BY region) AS region_total,
-- ORDER BY gives a stable sequence inside each region.
ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date, sale_id) AS row_in_region,
-- Ties on amount share the same rank, which is why West has two rows ranked 1.
RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS amount_rank_in_region,
-- ROWS makes the running total advance one physical row at a time.
SUM(amount) OVER (
PARTITION BY region
ORDER BY sale_date, sale_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total,
-- Gotcha: default frame often ends at the current row, so this is not the partition's true last value.
LAST_VALUE(amount) OVER (
PARTITION BY region
ORDER BY sale_date, sale_id
) AS default_last_value,
-- This version extends the frame to the end of the partition, which is usually what people meant.
LAST_VALUE(amount) OVER (
PARTITION BY region
ORDER BY sale_date, sale_id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS true_last_value
FROM sales
ORDER BY region, sale_date, sale_id;Follow-up & Tricky Questions:
PARTITION BY and GROUP BY? GROUP BY collapses rows into one row per group, while PARTITION BY keeps every row and only changes the window seen by the function.OVER () mean? It means the window is the entire filtered result set, so every row sees the same overall context.ORDER BY important in a window? It defines row sequence inside the partition, which is required for running totals, ranking, and value-at-position functions.row_number() or rank() over the partition, then filter in an outer query where the rank is less than or equal to 3.WHERE? Because SQL evaluates WHERE before window functions are computed, so you must place the windowed result in a subquery or CTE first.ROWS and RANGE? ROWS counts physical rows one by one; RANGE groups peers that share the same sort value, which can change results when there are ties.last_value often surprise people? Because its default frame usually ends at the current row, so it returns the current row's value unless you explicitly extend the frame to the end of the partition.row_number() need a frame? Usually no; ranking functions care about the ordering, not the frame, which is why the frame issue is more dangerous for aggregate-style window functions like sum or last_value.ORDER BY values? They become peers, so ranking functions may give the same rank, and RANGE-based frames may include both rows together.Common Mistakes:
OVER collapses rows. Correction: it preserves row detail and adds analytics per row.ORDER BY for running logic. Correction: without order, a running total or rank has no meaningful sequence.last_value without an explicit frame. Correction: extend the frame to UNBOUNDED FOLLOWING when you want the true last row of the partition.WHERE. Correction: compute it in a CTE or subquery, then filter outside.Memory Hook: GROUP BY is a blender; OVER is a glass window. A blender turns many rows into one smoothie. A glass window leaves every row visible and adds context on top.
Cheat Sheet:
OVER () = whole result set.PARTITION BY = split into buckets.ORDER BY = choose row order inside each bucket.ROWS = physical-row frame; best for running totals.RANGE = peer-based frame; watch out for ties.last_value often needs an explicit frame.Practice Tasks: