What does PARTITION BY do in SQL?
PARTITION BY splits a query result into logical groups for a window function, then restarts the calculation inside each group without collapsing the original rows. Use it when every detail row must remain visible but also needs a group-aware value such as rank, departmental average, running total, previous event, or top-N position.
The clause belongs inside OVER(...). It does not physically split a table, create database partitions, or reduce the result to one row per group. The database first forms the rowset produced by FROM, joins, filters, and grouping; the window operation then evaluates related rows from that rowset.
window_function(expression) OVER (
PARTITION BY group_column
ORDER BY sort_column
ROWS BETWEEN frame_start AND frame_end
)Build the right PARTITION BY mental model
A window specification has three independent decisions. PARTITION BY answers “which rows belong together?” ORDER BY answers “in what logical sequence should those rows be evaluated?” The frame answers “which subset around the current row should this function see?” Treating these as separate decisions prevents most subtle errors.
Choose the business boundary: customer, account, department, device, or a composite of several columns.
Choose a deterministic sequence. Add a stable tie-breaker when timestamps or amounts can repeat.
Decide whether the function should see the whole partition, all prior rows, a moving range, or peer rows.
Confirm that one result row still represents the intended entity. Window functions preserve rows and can expose duplicate joins.
Evaluation matters: window functions are generally evaluated after WHERE, GROUP BY, and HAVING. To filter on a calculated rank, compute it in a subquery or CTE and filter in the outer query.
PARTITION BY vs GROUP BY vs table partitioning
These concepts all use the word “group” or “partition,” but they solve different problems. Confusing them leads to incorrect page-level explanations and, more importantly, incorrect queries.
| Technique | What it changes | Row count | Typical use |
|---|---|---|---|
PARTITION BY | The scope of a window calculation | Normally preserves detail rows | Rank, running total, lag, group average |
GROUP BY | The output grain before window evaluation | Collapses rows into groups | Revenue per month, count per customer |
| Table partitioning | How stored data is divided and accessed | Does not define query output grain | Data lifecycle, pruning, operations |
A query may combine all three: read a physically partitioned sales table, aggregate it by day with GROUP BY, then calculate a rolling seven-day total with PARTITION BY region ORDER BY day. Each layer has a separate grain and cost.
A worked PARTITION BY SQL example
Assume an orders table contains one row per order. The business wants every order to remain visible, together with the customer's lifetime spend, the order's sequence, and a running spend total.
SELECT
order_id,
customer_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer_id
) AS customer_total,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date, order_id
) AS order_sequence,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date, order_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_spend
FROM orders;The first SUM has no ordering, so every row for a customer receives the same partition-wide total. ROW_NUMBER requires a logical sequence; adding order_id makes ties on order_date deterministic. The second SUM explicitly uses a row-based frame, so the running total advances one result row at a time.
Do not assume the final output is sorted. Ordering inside OVER(...) defines calculation order, not presentation order. Add a query-level ORDER BY customer_id, order_date, order_id when consumers require stable display order.
Five high-value PARTITION BY patterns
The clause becomes useful when it answers a specific analytical question. These patterns cover most production uses while showing where complexity enters.
| Question | Pattern | Main risk |
|---|---|---|
| What is each row's rank within a group? | ROW_NUMBER/RANK OVER (PARTITION BY ... ORDER BY ...) | Unresolved ties |
| What share of the group does this row represent? | amount / SUM(amount) OVER (PARTITION BY ...) | Zero denominators and duplicate rows |
| What happened immediately before? | LAG(value) OVER (PARTITION BY ... ORDER BY ...) | Ambiguous event sequence |
| What is the cumulative value so far? | SUM(...) OVER (... ROWS UNBOUNDED PRECEDING) | Implicit frame and peer behavior |
| Which are the top N rows in each group? | Rank in a CTE, filter outside | Filtering before ranking |
WITH ranked_orders AS (
SELECT
o.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY amount DESC, order_id
) AS rn
FROM orders o
)
SELECT *
FROM ranked_orders
WHERE rn <= 3;Window frames: the source of silent errors
A partition is not always the exact set of rows used by a function. Frame-sensitive functions operate on a subset of the current partition. When an ORDER BY appears and no frame is written, the database's default can produce cumulative behavior and may include peer rows that share the same sort value.
| Frame | Meaning | Use when |
|---|---|---|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | Every physical result row from partition start through current row | Deterministic running totals |
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW | Current row plus six preceding rows | Seven-observation moving metric |
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING | The complete partition | Partition-wide first or last value |
RANGE ... CURRENT ROW | Logical range that can include peers | Peer-aware business semantics are intentional |
For LAST_VALUE, the default frame is especially surprising: “last” can mean the last row in the current frame, not the last row in the entire partition. Write the full-partition frame explicitly when that is the intended meaning.
Choose partition keys, tie-breakers, and NULL behavior
Partitioning by one column is easy to read, but production business boundaries often require multiple columns. A tenant-aware account calculation may need PARTITION BY tenant_id, account_id; using only account_id could mix unrelated tenants.
- Test cardinality: a very high-cardinality key creates many tiny partitions; a low-cardinality key can create a few very large partitions.
- Define ties: add a unique or stable key after the business sort key when functions require deterministic row order.
- Handle NULL deliberately: NULL partition keys typically form a shared partition. Decide whether “unknown” rows belong together.
- Normalize keys before windowing: inconsistent casing, whitespace, time zones, or surrogate mappings can create unintended groups.
How PARTITION BY affects query complexity
PARTITION BY is not a performance hint. A database may need to sort or redistribute rows by partition and order keys before evaluating the window. The cost depends on the number of input rows after filtering, partition-size distribution, number of distinct window specifications, required sort orders, memory, data types, indexes, and database engine.
Reduce unnecessary complexity by filtering early when semantically valid, selecting only required columns, reusing identical named windows where supported, avoiding repeated expressions, and separating major grain changes into clear CTEs. Then inspect the actual execution plan rather than assuming an index eliminates every sort.
Tool boundary: a static complexity checker can identify structural signals such as multiple windows, nested queries, repeated partition clauses, broad projections, and deep joins. It cannot replace an engine-specific execution plan, runtime statistics, table sizes, or index analysis.
Validate a PARTITION BY query step by step
A syntactically valid window query can still return the wrong business answer. Validate the data grain, boundary, sequence, frame, and output separately.
- Freeze the intended grain.Write down what one result row represents before adding any window function.
- Create a tiny adversarial dataset.Include two partitions, duplicate timestamps, NULL keys, one-row groups, and unequal partition sizes.
- Run the base rowset alone.Check joins and filters before the window calculation can mask duplication.
- Add one window expression at a time.Compare hand-calculated expected values for boundary and tied rows.
- Make order and frame explicit.Do not rely on defaults when the business meaning is row-by-row, moving, or whole-partition.
- Inspect structure, then runtime.Use static complexity inspection to find risky constructs, then verify the execution plan and representative production volumes.
Inspect your PARTITION BY query before review
Prepare the complete SQL statement—not only the OVER clause—so joins, nested layers, repeated windows, and projections can be assessed together. The InfiniSynapse SQL Complexity Checker provides a structural review that helps you decide where simplification or deeper engine-level testing is needed.
Check PARTITION BY behavior by SQL dialect
The central idea is portable, but supported functions, named-window syntax, frame options, NULL handling, optimizer behavior, and filtering features vary. Always test against the exact engine and version used in production.
| Engine | What to verify | Official reference |
|---|---|---|
| PostgreSQL | Default frames, named windows, peer behavior, and where window functions are allowed | PostgreSQL window functions |
| SQL Server | OVER syntax, required ordering, supported frame clauses, and plan operators | Microsoft OVER clause |
| MySQL 8.4 | Available window functions, frame syntax, named windows, and optimizer restrictions | MySQL window syntax |
Common PARTITION BY mistakes and repairs
Repair: validate composite keys and tenant boundaries with known examples.
Repair: append a stable tie-breaker to the window order.
Repair: specify ROWS or the intended complete-partition frame explicitly.
Repair: calculate the window result in a CTE, then filter outside.
Repair: consolidate identical definitions and question whether every sort is necessary.
Repair: inspect plans, spills, row counts, and production-like distributions.
PARTITION BY review checklist
- One output row still represents the intended entity.
- The partition key matches the complete business boundary.
- The window order is deterministic wherever row sequence matters.
- The frame is explicit for running, moving, first, and last calculations.
- NULL keys, ties, one-row partitions, and skewed partitions are tested.
- Window results are filtered at the correct query layer.
- Repeated window definitions are consolidated where practical.
- Static structural checks are followed by engine-specific plan analysis.
PARTITION BY SQL FAQ
It divides the current query rowset into logical groups for a window function. The calculation restarts in each group, while detail rows remain in the result.
GROUP BY changes the result grain and usually returns one row per group. PARTITION BY defines the scope of a window calculation without normally collapsing rows.
Yes, when order is irrelevant—for example a partition-wide average. Ranking, navigation, and running calculations depend on ordering and should use a deterministic key.
An implicit RANGE-style frame may include peer rows with the same ordering value. Use an explicit ROWS frame and a tie-breaker for row-by-row accumulation.
Not automatically. It can require sorting or redistribution. Filtered row volume, partition skew, windows, indexes, memory, and the execution plan determine actual performance.
Official SQL references
Use the documentation for your exact database version as the final authority. The examples in this guide emphasize portable concepts; engines can differ in syntax, frames, optimizations, and restrictions.