Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

PL/SQL & T-SQL (Oracle and SQL Server)  Module 5 – Window functions in T-SQL

T-SQL ranking and offset window functions

Number rows with ROW_NUMBER, RANK, DENSE_RANK and NTILE, read other rows with LAG and LEAD, and filter window results in a CTE, as T-SQL has no QUALIFY.

  • Intermediate
  • 20 minutes
  • Examples run with oracle-free 23.26.3.0.0, mssql 17.0.5005.3, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use ROW_NUMBER, RANK, DENSE_RANK and NTILE with PARTITION BY and ORDER BY
  • Use LAG and LEAD with offsets and defaults
  • Solve top-N per group and de-duplication without QUALIFY
  • State where window functions are allowed in a T-SQL query

Before you start

What you’ll learn · Before you start

What you will learn

  • Use ROW_NUMBER, RANK, DENSE_RANK and NTILE with PARTITION BY and ORDER BY
  • Use LAG and LEAD with offsets and defaults
  • Solve top-N per group and de-duplication without QUALIFY
  • State where window functions are allowed in a T-SQL query

Before you start

On this page

Pune sold 20 units each of GHEE-500 and RICE-5K this month, and a top-2 report built on ROW_NUMBER shows only one of them, picked by nothing more than the order in which the tied rows happened to arrive. T-SQL has four ranking functions that treat such a tie four different ways, and two offset functions, LAG and LEAD, that read the row before or after the current one.

You will number rows with the four functions, read other rows with LAG and LEAD, filter on window results in a CTE, since T-SQL has no QUALIFY, and remove duplicates by keeping row 1 of each key. The sql track’s window basics cover OVER and PARTITION BY in general; this lesson keeps to T-SQL’s own rules.

Four ranking functions differ in how they number ties and groups

A ranking function: A window function that numbers the rows of each partition in the order of its OVER clause: ROW_NUMBER, RANK, DENSE_RANK and NTILE in T-SQL, which differ in how they number ties. gives each row a number within its window partition: The group of rows that PARTITION BY gives a window function to work on; numbering, offsets and frames start again in each partition, and without PARTITION BY the whole result is one partition., the group of rows that PARTITION BY puts it in, in the order that the ORDER BY of OVER sets, which all four require. Microsoft’s pages define the four. ROW_NUMBER numbers every row 1, 2, 3, ties or not. RANK gives tied rows the same number and skips the next ones, so a tie at second place runs 1, 2, 2, 4. DENSE_RANK repeats the number for ties and goes on without a gap: 1, 2, 2, 3. NTILE(n) cuts the partition into n groups whose sizes differ by one at most, the larger groups first. All four return bigint. Step through the lab’s two cities:

Interactive · step by step Four numbers for the same products Step through the two city partitions and watch how each function numbers the tied products.
Four numbers for the same products, step 6 of 6. Showing TEA-250: 90 (NTILE 1), GHEE-500: 20 (NTILE 1), RICE-5K: 20 (NTILE 2), SALT-1K: 10 (NTILE 2), RICE-5K: 70 (NTILE 1), TEA-250: 70 (NTILE 1), GHEE-500: 40 (NTILE 2). In focus: RICE-5K: 20, SALT-1K: 10, GHEE-500: 40.TEA-250: 90NTILE 1GHEE-500: 20NTILE 1RICE-5K: 20NTILE 2SALT-1K: 10NTILE 2RICE-5K: 70NTILE 1TEA-250: 70NTILE 1GHEE-500: 40NTILE 2

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One partition per city. PARTITION BY city splits the seven rows into Pune's four and Kochi's three. Every function starts again at 1 in each partition.
  2. Ordered by units, then product. ORDER BY units DESC puts the best seller first. GHEE-500 and RICE-5K tie in Pune, and RICE-5K and TEA-250 tie in Kochi; the product code breaks the tie for ROW_NUMBER.
  3. ROW_NUMBER never repeats. ROW_NUMBER numbers every row 1, 2, 3 and so on, ties or not. Without the tie-breaker, which of the tied rows gets the lower number is not guaranteed.
  4. RANK repeats, then skips. RANK repeats the number for tied rows and then skips ahead by the size of the tie. Pune runs 1, 2, 2, 4, and Kochi 1, 1, 3.
  5. DENSE_RANK leaves no gap. DENSE_RANK also repeats a number for ties but carries on with the next one. Pune runs 1, 2, 2, 3, and Kochi 1, 1, 2.
  6. NTILE cuts into groups. NTILE(2) splits each partition into two groups whose sizes differ by one at most, larger groups first. Pune gets two and two, Kochi two and one.

The script ranks the seven rows of the figure:

ranking_ties.sql: four ranking functions on two cities SQL · ranking/ranking_ties.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab at compatibility level 170. Client: sqlcmd.
-- The four ranking functions on the units of ranking_model.py: two cities, each a partition, with a tie in each.
-- product is the tie-breaker, so ROW_NUMBER gives the same numbers on every run.
SET NOCOUNT ON;
CREATE TABLE #units (city nvarchar(10) NOT NULL, product varchar(12) NOT NULL, units int NOT NULL,
                     PRIMARY KEY (city, product));
INSERT INTO #units VALUES
  (N'Pune', 'TEA-250', 90), (N'Pune', 'RICE-5K', 20), (N'Pune', 'GHEE-500', 20), (N'Pune', 'SALT-1K', 10),
  (N'Kochi', 'RICE-5K', 70), (N'Kochi', 'TEA-250', 70), (N'Kochi', 'GHEE-500', 40);

SELECT city, product, units,
       ROW_NUMBER() OVER (PARTITION BY city ORDER BY units DESC, product) AS row_number_,   -- Pune 1, 2, 3, 4
       RANK()       OVER (PARTITION BY city ORDER BY units DESC)          AS rank_,         -- Pune 1, 2, 2, 4
       DENSE_RANK() OVER (PARTITION BY city ORDER BY units DESC)          AS dense_rank_,   -- Pune 1, 2, 2, 3
       NTILE(2)     OVER (PARTITION BY city ORDER BY units DESC, product) AS half           -- Pune 1, 1, 2, 2
FROM #units
ORDER BY city, units DESC, product;
DROP TABLE #units;
GO
ranking_ties.sql, run in the lab Python · ranking_ties_session.py
"""ranking_ties.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice schema."""

from mssql_session import Lab

with Lab() as lab:
    lab.sqlcmd("ranking/ranking_ties.sql")

Output

$ sqlcmd -S localhost -U sa -C -d lab -i ranking/ranking_ties.sql
city  product  units row_number_ rank_ dense_rank_ half
----- -------- ----- ----------- ----- ----------- ----
Kochi RICE-5K     70           1     1           1    1
Kochi TEA-250     70           2     1           1    1
Kochi GHEE-500    40           3     3           2    2
Pune  TEA-250     90           1     1           1    1
Pune  GHEE-500    20           2     2           2    1
Pune  RICE-5K     20           3     2           2    2
Pune  SALT-1K     10           4     4           3    2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ranking_ties_session.py

The model numbers the same rows by the same rules. Press Edit, change RICE-5K’s 20 units in Pune to 90, and run it again:

Four numbers for the same seven rows Python · ranking/ranking_model.py
"""ROW_NUMBER, RANK, DENSE_RANK and NTILE(2) on the same units.

The rules are those of Microsoft's ranking function pages: ROW_NUMBER numbers
every row, RANK gives tied rows the same number and skips the next ones,
DENSE_RANK gives tied rows the same number without gaps, and NTILE(n) cuts a
partition into n groups whose sizes differ by at most one, larger groups
first. Rows are ordered by units, highest first, with the product code as
the tie-breaker. Change RICE-5K's 20 units in Pune to 90 and run it again.
"""

UNITS = [  # (city, product, units sold this month)
    ("Pune", "TEA-250", 90),
    ("Pune", "RICE-5K", 20),
    ("Pune", "GHEE-500", 20),
    ("Pune", "SALT-1K", 10),
    ("Kochi", "RICE-5K", 70),
    ("Kochi", "TEA-250", 70),
    ("Kochi", "GHEE-500", 40),
]


def ntile(n_rows, groups):
    """Group number of each row: sizes differ by one at most, larger groups first."""
    base, extra = divmod(n_rows, groups)
    numbers = []
    for g in range(groups):
        numbers += [g + 1] * (base + (1 if g < extra else 0))
    return numbers


print("city   product   units  ROW_NUMBER  RANK  DENSE_RANK  NTILE(2)")
for city in sorted({c for c, _, _ in UNITS}):
    rows = sorted((r for r in UNITS if r[0] == city), key=lambda r: (-r[2], r[1]))
    tiles = ntile(len(rows), 2)
    rank = dense = 0
    previous = None
    for n, (_, product, units) in enumerate(rows, start=1):
        if units != previous:
            rank, dense, previous = n, dense + 1, units
        print(f"{city:<6} {product:<9} {units:>5}  {n:>10}  {rank:>4}  {dense:>10}  {tiles[n - 1]:>8}")

cities = len({c for c, _, _ in UNITS})
summary = f"{len(UNITS)} rows in {cities} partitions; numbering starts again at 1 in each city."
print(summary)

Output

city   product   units  ROW_NUMBER  RANK  DENSE_RANK  NTILE(2)
Kochi  RICE-5K      70           1     1           1         1
Kochi  TEA-250      70           2     1           1         1
Kochi  GHEE-500     40           3     3           2         2
Pune   TEA-250      90           1     1           1         1
Pune   GHEE-500     20           2     2           2         1
Pune   RICE-5K      20           3     2           2         2
Pune   SALT-1K      10           4     4           3         2
7 rows in 2 partitions; numbering starts again at 1 in each city.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ranking_model.py

Pune’s tie at 20 units shows the three behaviours: ROW_NUMBER 2 and 3, RANK 2 and 2 and then 4, DENSE_RANK 2 and 2 and then 3. After the edit RICE-5K ties with TEA-250 at the top, and both RANK and DENSE_RANK give them 1.

ROW_NUMBER’s order among tied rows is not guaranteed: Microsoft’s page promises the same numbers on every run only when the partition and ORDER BY columns together are unique. A tie-breaker: An extra column at the end of an ORDER BY, such as a key, that makes the order unique, so that rows with equal values in the other columns always come in the same order., here product, added at the end of the ORDER BY, makes them unique.

Checkpoint

Check: Ranking functions and ties

  1. Question 1 of 2 One partition holds the units 90, 20, 20 and 10, ordered by units, highest first. What does RANK() give the row with 10 units?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 4

    The two rows with 20 share rank 2, and the next rank counts the three rows before it, so 10 gets 4. DENSE_RANK would give it 3.

  2. Question 2 of 2 Why does the lesson's script order ROW_NUMBER by units DESC, product rather than by units DESC alone?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So that tied rows get the same numbers on every run

    A unique ORDER BY inside OVER is the tie-breaker that makes ROW_NUMBER deterministic.

LAG and LEAD read another row of the partition, with a default at the edges

LAG and LEAD are offset function: A window function that reads a value from another row of the partition at a given distance from the current row, such as LAG for an earlier row and LEAD for a later one.: LAG(units, 1, 0) reads units from the row one place before the current one in the window’s order, and LEAD from the row after it, without a self-join. Microsoft’s LAG page sets the rules for the two optional arguments. The offset defaults to 1 and may not be negative. The default is the value returned when the offset points past the edge of the partition, NULL when none is given, and it must be type-compatible with the first argument. Neither function takes a frame. The script reads each city’s monthly sales of TEA-250:

lag_lead.sql: the month before, the month after, two months back SQL · offset/lag_lead.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab at compatibility level 170. Client: sqlcmd.
-- LAG and LEAD over each city's monthly units of TEA-250, as lag_model.py computes them. The default 0 of
-- LAG(units, 1, 0) fills the first month of each partition; without it the first month gets NULL.
SET NOCOUNT ON;
CREATE TABLE #monthly (city nvarchar(10) NOT NULL, month_no int NOT NULL, units int NOT NULL,
                       PRIMARY KEY (city, month_no));
INSERT INTO #monthly VALUES (N'Pune', 1, 60), (N'Pune', 2, 75), (N'Pune', 3, 75), (N'Pune', 4, 90),
                            (N'Kochi', 1, 50), (N'Kochi', 2, 50), (N'Kochi', 3, 65);

SELECT city, month_no, units,
       LAG(units, 1, 0) OVER (PARTITION BY city ORDER BY month_no)         AS previous_or_0,  -- Pune 0, 60, 75, 75
       units - LAG(units) OVER (PARTITION BY city ORDER BY month_no)       AS change,         -- Pune NULL, 15, 0, 15
       LEAD(units)      OVER (PARTITION BY city ORDER BY month_no)         AS next_month,     -- Pune 75, 75, 90, NULL
       LAG(units, 2)    OVER (PARTITION BY city ORDER BY month_no)         AS two_back        -- Pune NULL, NULL, 60, 75
FROM #monthly
ORDER BY city DESC, month_no;
DROP TABLE #monthly;
GO
lag_lead.sql, run in the lab Python · lag_lead_session.py
"""lag_lead.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice schema."""

from mssql_session import Lab

with Lab() as lab:
    lab.sqlcmd("offset/lag_lead.sql")

Output

$ sqlcmd -S localhost -U sa -C -d lab -i offset/lag_lead.sql
city  month_no units previous_or_0 change next_month two_back
----- -------- ----- ------------- ------ ---------- --------
Pune         1    60             0   NULL         75     NULL
Pune         2    75            60     15         75     NULL
Pune         3    75            75      0         90       60
Pune         4    90            75     15       NULL       75
Kochi        1    50             0   NULL         50     NULL
Kochi        2    50            50      0         65     NULL
Kochi        3    65            50     15       NULL       50

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lag_lead_session.py

The model computes the same columns, except that its change from the month before subtracts LAG(units, 1, 0) where the script subtracts LAG(units). Press Edit, set DEFAULT = None, and run it again:

LAG and LEAD over two cities Python · offset/lag_model.py
"""LAG and LEAD over each city's monthly sales of TEA-250.

LAG(units, offset, default) reads the row offset rows before the current one
in the partition and LEAD the row after it; past the edge of the partition
they return the default, NULL unless one is given. The rules are those of
Microsoft's LAG and LEAD pages. Change the default of LAG from 0 to None and
run it again.
"""

SALES = {  # city -> units of TEA-250 in months 1, 2, 3 and 4
    "Pune": [60, 75, 75, 90],
    "Kochi": [50, 50, 65],
}
DEFAULT = 0  # the third argument of LAG(units, 1, 0)


def lag(values, i, offset=1, default=None):
    return values[i - offset] if i - offset >= 0 else default


def lead(values, i, offset=1, default=None):
    return values[i + offset] if i + offset < len(values) else default


def show(value):
    return "NULL" if value is None else str(value)


print("city   month  units  LAG(1, default)  change  LEAD  LAG(2)")
for city, units in SALES.items():
    for i, u in enumerate(units):
        before = lag(units, i, 1, DEFAULT)
        change = None if before is None else u - before
        print(f"{city:<6} {i + 1:>5}  {u:>5}  {show(before):>15}  {show(change):>6}  {show(lead(units, i)):>4}  {show(lag(units, i, 2)):>6}")

first_rows = len(SALES)
note = f"{first_rows} first months get the default as their previous value."
print(note)

Output

city   month  units  LAG(1, default)  change  LEAD  LAG(2)
Pune       1     60                0      60    75    NULL
Pune       2     75               60      15    75    NULL
Pune       3     75               75       0    90      60
Pune       4     90               75      15  NULL      75
Kochi      1     50                0      50    50    NULL
Kochi      2     50               50       0    65    NULL
Kochi      3     65               50      15  NULL      50
2 first months get the default as their previous value.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lag_model.py

Each city’s first month has no month before it, so LAG(units, 1, 0) returns the default 0, and the change column shows Pune’s first month as a rise of 60 from nothing. After the edit the first month’s previous value and its change are NULL, which is what a report should show for a month with no history. Each partition starts again: Kochi’s first month gets the default as well.

A default of 0 inside a growth rate

Wrong: units - LAG(units, 1, 0) OVER (…) or a growth rate divided by LAG(units, 1, 0). The first month shows a rise from zero, and a division by the default fails with error 8134. Right: leave the default out, so the first month’s change is NULL, and handle it in the report; a default fits only where 0 is a true value.

SQL Server 2022 adds IGNORE NULLS to LAG and LEAD, which the frames lesson uses to carry a value forward.

Checkpoint

Check: LAG, LEAD, offsets and defaults

  1. Question 1 of 2 What does LAG(units, 1, 0) OVER (PARTITION BY city ORDER BY month_no) return for Kochi's first month?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0, the default, because no earlier month exists in Kochi's partition

    The default fills the edge of every partition, so each city's first month gets 0.

  2. Question 2 of 2 What does LEAD(units) OVER (PARTITION BY city ORDER BY month_no) return for Pune's last month?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: NULL

    The lag_model.py output shows NULL in the LEAD column of each city's last month.

A window function runs after WHERE, so its filter goes in a CTE

Microsoft’s SELECT page lists the order in which SQL Server binds a query: FROM, ON, JOIN, WHERE, GROUP BY, WITH CUBE or WITH ROLLUP, HAVING, SELECT, DISTINCT, ORDER BY and TOP. Window functions are computed in the SELECT step, so WHERE runs before they have a value. Error 4108 says so in words: windowed functions can only appear in the SELECT or ORDER BY clauses. Some engines add a QUALIFY clause to filter on window results; Oracle AI Database 26ai has one. SQL Server does not: Microsoft’s SELECT page lists QUALIFY for Fabric Data Warehouse only. Follow a query through the clauses:

Interactive · stage by stage Where a window function is computed in a SELECT Step through the clauses in the order SQL Server binds them, and see why WHERE cannot use a window function.
Where a window function is computed in a SELECT, step 5 of 5. Showing FROM and JOIN, WHERE, GROUP BY and HAVING, SELECT, ORDER BY and TOP. In focus: ORDER BY and TOP.FROM and JOINthe rows of the tablesWHEREfilters single rowsGROUP BY and HAVINGgroups and their filterSELECTwindow functions hereORDER BY and TOPsort and limit

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The rows come first. SQL Server binds FROM, ON and JOIN first, so every later clause can use the tables' columns.
  2. WHERE runs before any window. WHERE filters single rows before any window function has a value. That is why a ROW_NUMBER() in WHERE fails with error 4108, and the error names the only places allowed.
  3. Groups and their filter. GROUP BY and HAVING come next. A window function may use the grouped rows, such as RANK() OVER (ORDER BY SUM(units) DESC), because it runs after them.
  4. Window functions in SELECT. Window functions are computed in the SELECT step, over the rows that are left after WHERE, GROUP BY and HAVING.
  5. ORDER BY can read them. ORDER BY and TOP come last, so ORDER BY may use a window function or its alias. To filter on one, compute it in a CTE or derived table and filter in the outer query.

Both refusals, and Oracle’s clause, are in the scripts:

A window function in WHERE and in QUALIFY, and Oracle's QUALIFY

SQL (where_errors.sql) · topn/where_errors.sql

-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab at compatibility level 170. Client: sqlcmd.
-- Two ways to filter on a window function that T-SQL refuses, each in a batch of its own: in WHERE (error 4108,
-- "Windowed functions can only appear in the SELECT or ORDER BY clauses.") and with QUALIFY, which SQL Server does
-- not have (a syntax error). The CTE in top_n_per_city.sql is the form that works.
SET NOCOUNT ON;
CREATE TABLE #units (city nvarchar(10), product varchar(12), units int);
INSERT INTO #units VALUES (N'Pune', 'TEA-250', 90), (N'Pune', 'RICE-5K', 20), (N'Kochi', 'RICE-5K', 70);
GO
SELECT city, product FROM #units
WHERE ROW_NUMBER() OVER (PARTITION BY city ORDER BY units DESC) = 1;          -- error 4108
GO
SELECT city, product FROM #units
QUALIFY ROW_NUMBER() OVER (PARTITION BY city ORDER BY units DESC) = 1;        -- syntax error: no QUALIFY clause
GO
DROP TABLE #units;
GO

SQL (qualify_oracle.sql) · topn/qualify_oracle.sql

-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus.
-- Oracle AI Database 26ai has a QUALIFY clause, which filters on window functions in the same query block; a port
-- of this query to T-SQL moves the filter into a CTE or a derived table.
WITH units (city, product, units) AS (
  SELECT 'Pune', 'TEA-250', 90 FROM dual UNION ALL SELECT 'Pune', 'RICE-5K', 20 FROM dual UNION ALL
  SELECT 'Pune', 'GHEE-500', 20 FROM dual UNION ALL SELECT 'Pune', 'SALT-1K', 10 FROM dual UNION ALL
  SELECT 'Kochi', 'RICE-5K', 70 FROM dual UNION ALL SELECT 'Kochi', 'TEA-250', 70 FROM dual UNION ALL
  SELECT 'Kochi', 'GHEE-500', 40 FROM dual
)
SELECT city, product, units
FROM units
QUALIFY RANK() OVER (PARTITION BY city ORDER BY units DESC) <= 2
ORDER BY city, units DESC, product;
where_errors.sql and qualify_oracle.sql, run in the labs

SQL Server · where_errors_session.py

"""where_errors.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice schema."""

from mssql_session import Lab

with Lab() as lab:
    lab.sqlcmd("topn/where_errors.sql", errors=2)

Output

$ sqlcmd -S localhost -U sa -C -d lab -i topn/where_errors.sql
Msg 4108, Level 15, State 1, Server lab-mssql, Line 2
Windowed functions can only appear in the SELECT or ORDER BY clauses.
Msg 102, Level 15, State 1, Server lab-mssql, Line 2
Incorrect syntax near 'ROW_NUMBER'.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 where_errors_session.py

Oracle · qualify_oracle_session.py

"""qualify_oracle.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""

from oracle_session import Lab

with Lab() as lab:
    lab.sqlplus("topn/qualify_oracle.sql")

Output

$ sqlplus -s lab@//localhost/FREEPDB1 @topn/qualify_oracle.sql
CITY  PRODUCT       UNITS
----- -------- ----------
Kochi RICE-5K          70
Kochi TEA-250          70
Pune  TEA-250          90
Pune  GHEE-500         20
Pune  RICE-5K          20

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 qualify_oracle_session.py

The T-SQL form computes the number in a CTE and filters in the outer query. That is the shape of every top-N per group: number each city’s products by units, then keep the rows numbered up to N. Which function to use depends on ties; answer three questions about your report:

Interactive · answer and compare Which ranking function fits your query? Answer three questions about ties and row counts; the table then shows which functions still fit your report.
Must tied rows share a place?
Must a top N return exactly N rows?
Do you need equal-sized groups rather than places?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ranking functions on the lesson's units, against ties, row counts and gaps
Function Tied rows Rows for a top 2 Gaps after a tie
ROW_NUMBER Gives Pune: 1, 2, 3, 4 Poor split by the tie-breaker Good exactly 2 rows in every city Good never any gaps
RANK Gives Pune: 1, 2, 2, 4 Good share one number Mixed 3 rows in Pune, 2 in Kochi Mixed skips after a tie: 1, 2, 2, 4
DENSE_RANK Gives Pune: 1, 2, 2, 3 Good share one number Mixed 3 rows in each city Good no gaps: 1, 2, 2, 3
NTILE(2) Gives Pune: 1, 1, 2, 2 Poor may split a tie across groups Poor groups by size, not a top N Good group numbers run 1 to n
How the answers decide
  • “Must tied rows share a place?” Yes, ties share a place: rules out ROW_NUMBER. ROW_NUMBER gives every row its own number, so one of two tied products drops out of a top N.
  • “Must a top N return exactly N rows?” Yes, exactly N: rules out RANK and DENSE_RANK. Ties add rows to a top N under RANK and DENSE_RANK, as Pune's three rows show.
  • “Do you need equal-sized groups rather than places?” No, places: rules out NTILE(2). NTILE makes groups of nearly equal size; it does not number places.
  • “Do you need equal-sized groups rather than places?” Yes, equal groups: rules out ROW_NUMBER, RANK and DENSE_RANK. Only NTILE divides a partition into a given number of groups.
  • “Must tied rows share a place?” Yes, ties share a place and “Must a top N return exactly N rows?” No, ties may add rows: adds a caveat to RANK. RANK leaves a gap after a tie: Pune's places run 1, 2, 2, 4, so a list of places 1 to 3 has no place 3.

The model runs the top-2 query with each of the three numbering functions. Press Edit, set TOP = 1, and run it again:

Top 2 per city with three numbering functions Python · topn/top_n_model.py
"""Top 2 products per city with three numbering functions.

The query numbers each city's products by units, highest first, in a CTE and
keeps the rows numbered 1 or 2 in the outer query. ROW_NUMBER returns exactly
two rows a city and drops one of two tied products by its tie-breaker; RANK and
DENSE_RANK keep ties, and DENSE_RANK can return more rows still. Set TOP to 1
and run it again.
"""

UNITS = [
    ("Pune", "TEA-250", 90), ("Pune", "RICE-5K", 20), ("Pune", "GHEE-500", 20), ("Pune", "SALT-1K", 10),
    ("Kochi", "RICE-5K", 70), ("Kochi", "TEA-250", 70), ("Kochi", "GHEE-500", 40),
]
TOP = 2


def numbered(rows, kind):
    out, rank, dense, previous = [], 0, 0, None
    for n, (product, units) in enumerate(rows, start=1):
        if units != previous:
            rank, dense, previous = n, dense + 1, units
        out.append((product, units, {"ROW_NUMBER": n, "RANK": rank, "DENSE_RANK": dense}[kind]))
    return out


for kind in ("ROW_NUMBER", "RANK", "DENSE_RANK"):
    print(f"{kind} <= {TOP}")
    for city in sorted({c for c, _, _ in UNITS}):
        rows = sorted(((p, u) for c, p, u in UNITS if c == city), key=lambda r: (-r[1], r[0]))
        kept = [f"{p} ({u})" for p, u, n in numbered(rows, kind) if n <= TOP]
        print(f"  {city:<6} {len(kept)} row{'' if len(kept) == 1 else 's'}: {', '.join(kept)}")

Output

ROW_NUMBER <= 2
  Kochi  2 rows: RICE-5K (70), TEA-250 (70)
  Pune   2 rows: TEA-250 (90), GHEE-500 (20)
RANK <= 2
  Kochi  2 rows: RICE-5K (70), TEA-250 (70)
  Pune   3 rows: TEA-250 (90), GHEE-500 (20), RICE-5K (20)
DENSE_RANK <= 2
  Kochi  3 rows: RICE-5K (70), TEA-250 (70), GHEE-500 (40)
  Pune   3 rows: TEA-250 (90), GHEE-500 (20), RICE-5K (20)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 top_n_model.py

ROW_NUMBER returns exactly two rows a city and drops RICE-5K in Pune, RANK keeps Pune’s tie and returns three rows there, and DENSE_RANK returns three in each city, because Kochi’s third product is only the second distinct amount. After the edit Kochi’s top two products tie for first, so RANK and DENSE_RANK both return two rows for it. For a single group, TOP (2) WITH TIES … ORDER BY units DESC does what RANK() <= 2 does:

top_n_per_city.sql: a CTE filter, and TOP WITH TIES for one city SQL · topn/top_n_per_city.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab at compatibility level 170. Client: sqlcmd.
-- Top 2 products per city: the window function is computed in a CTE and filtered in the outer query, because a
-- WHERE cannot read it. ROW_NUMBER keeps exactly two rows a city; RANK keeps Pune's tie at second place.
SET NOCOUNT ON;
CREATE TABLE #units (city nvarchar(10) NOT NULL, product varchar(12) NOT NULL, units int NOT NULL,
                     PRIMARY KEY (city, product));
INSERT INTO #units VALUES
  (N'Pune', 'TEA-250', 90), (N'Pune', 'RICE-5K', 20), (N'Pune', 'GHEE-500', 20), (N'Pune', 'SALT-1K', 10),
  (N'Kochi', 'RICE-5K', 70), (N'Kochi', 'TEA-250', 70), (N'Kochi', 'GHEE-500', 40);

WITH ranked AS (
  SELECT city, product, units,
         ROW_NUMBER() OVER (PARTITION BY city ORDER BY units DESC, product) AS n,
         RANK()       OVER (PARTITION BY city ORDER BY units DESC)          AS place
  FROM #units
)
SELECT city, product, units, n, place
FROM ranked
WHERE place <= 2                                -- Pune: TEA-250, GHEE-500, RICE-5K; Kochi: RICE-5K, TEA-250
ORDER BY city, place, product;

-- One city only: TOP (2) WITH TIES keeps the rows that tie with the last one, as RANK <= 2 does.
SELECT TOP (2) WITH TIES product, units FROM #units WHERE city = N'Pune' ORDER BY units DESC;   -- 3 rows
DROP TABLE #units;
GO
top_n_per_city.sql, run in the lab Python · top_n_per_city_session.py
"""top_n_per_city.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice schema."""

from mssql_session import Lab

with Lab() as lab:
    lab.sqlcmd("topn/top_n_per_city.sql")

Output

$ sqlcmd -S localhost -U sa -C -d lab -i topn/top_n_per_city.sql
city  product  units n place
----- -------- ----- - -----
Kochi RICE-5K     70 1     1
Kochi TEA-250     70 2     1
Pune  TEA-250     90 1     1
Pune  GHEE-500    20 2     2
Pune  RICE-5K     20 3     2
product  units
-------- -----
TEA-250     90
GHEE-500    20
RICE-5K     20

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 top_n_per_city_session.py

Checkpoint

Check: Filtering on a window result

  1. Question 1 of 2 What does SQL Server do with SELECT city, product FROM units WHERE ROW_NUMBER() OVER (PARTITION BY city ORDER BY units DESC) = 1?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It refuses the query with error 4108, because WHERE runs before window functions are computed

    Compute ROW_NUMBER in a CTE and write WHERE n = 1 in the query that reads the CTE.

  2. Question 2 of 2 The top-2 query keeps the rows with RANK() <= 2. How many rows does it return for Pune, whose units are 90, 20, 20 and 10?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 3

    90 has rank 1 and both 20s have rank 2, so three rows have a rank of 2 or less. ROW_NUMBER would return 2.

De-duplicating keeps row 1 of each key and deletes the rest

A feed that delivers the same order twice, or an old status beside the new one, leaves several rows for one key. Numbering the rows of each key newest first, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC, event_id DESC), gives the row to keep the number 1. Microsoft’s CTE page allows a DELETE after a CTE, so the statement can delete through the CTE, DELETE FROM numbered WHERE n > 1, and OUTPUT deleted.… lists what it removed:

latest_status_per_order.sql: keep the newest status of each order SQL · dedupe/latest_status_per_order.sql
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab at compatibility level 170. Client: sqlcmd.
-- The feed of dedupe_model.py: six status rows for three orders. ROW_NUMBER in a CTE numbers each order's rows
-- newest first, with event_id as the tie-breaker; the DELETE through the CTE removes every row numbered above 1 and
-- OUTPUT shows what it removed.
SET NOCOUNT ON;
CREATE TABLE #order_status (event_id int PRIMARY KEY, order_id int NOT NULL, status nvarchar(10) NOT NULL,
                            updated_at datetime2(0) NOT NULL);
INSERT INTO #order_status VALUES
  (1, 101, N'new', '2026-03-12 09:00'), (2, 101, N'paid', '2026-03-12 09:05'), (3, 101, N'paid', '2026-03-12 09:05'),
  (4, 102, N'new', '2026-03-12 09:01'), (5, 102, N'paid', '2026-03-12 09:30'), (6, 103, N'new', '2026-03-12 09:10');

WITH numbered AS (
  SELECT event_id, order_id, status,
         ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC, event_id DESC) AS n
  FROM #order_status
)
DELETE FROM numbered
OUTPUT deleted.event_id, deleted.order_id, deleted.status                    -- events 1, 2 and 4
WHERE n > 1;

SELECT event_id, order_id, status FROM #order_status ORDER BY order_id;     -- events 3, 5 and 6
DROP TABLE #order_status;
GO
latest_status_per_order.sql, run in the lab Python · latest_status_per_order_session.py
"""latest_status_per_order.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice
schema."""

from mssql_session import Lab

with Lab() as lab:
    lab.sqlcmd("dedupe/latest_status_per_order.sql")

Output

$ sqlcmd -S localhost -U sa -C -d lab -i dedupe/latest_status_per_order.sql
event_id order_id status
-------- -------- ------
       2      101 paid
       1      101 new
       4      102 new
event_id order_id status
-------- -------- ------
       3      101 paid
       5      102 paid
       6      103 new

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 latest_status_per_order_session.py

The model replays the six rows. Press Edit, make order_key return (updated_at,) without event_id, and run it again:

Kept and deleted rows of the feed Python · dedupe/dedupe_model.py
"""Keeping the newest status of each order and deleting the rest.

A feed delivered six status rows for three orders, one of them twice at the
same minute. ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC,
event_id DESC) gives each order's newest row the number 1, with event_id as the
tie-breaker for rows of the same minute; a DELETE through the CTE removes every
row numbered above 1. Remove event_id from the ORDER BY tuple and run it again.
"""

EVENTS = [  # (event_id, order_id, status, updated_at)
    (1, 101, "new", "09:00"),
    (2, 101, "paid", "09:05"),
    (3, 101, "paid", "09:05"),
    (4, 102, "new", "09:01"),
    (5, 102, "paid", "09:30"),
    (6, 103, "new", "09:10"),
]


def order_key(event):
    event_id, _, _, updated_at = event
    return (updated_at, event_id)  # sorted descending below: newest first, then the higher id


kept, deleted = [], []
for order_id in sorted({e[1] for e in EVENTS}):
    rows = sorted((e for e in EVENTS if e[1] == order_id), key=order_key, reverse=True)
    for n, event in enumerate(rows, start=1):
        (kept if n == 1 else deleted).append((event[0], order_id, event[2], event[3], n))

for label, rows in (("kept", kept), ("deleted", deleted)):
    print(f"{label}: {len(rows)} rows")
    for event_id, order_id, status, updated_at, n in rows:
        print(f"  event {event_id}  order {order_id}  {status:<4} {updated_at}  row number {n}")

Output

kept: 3 rows
  event 3  order 101  paid 09:05  row number 1
  event 5  order 102  paid 09:30  row number 1
  event 6  order 103  new  09:10  row number 1
deleted: 3 rows
  event 2  order 101  paid 09:05  row number 2
  event 1  order 101  new  09:00  row number 3
  event 4  order 102  new  09:01  row number 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dedupe_model.py

Order 101 has two paid rows at 09:05, and the tie-breaker keeps event 3. After the edit the two rows tie, and the model keeps event 2, the one it happened to read first; SQL Server gives no promise either way. The tie-breaker is what makes a de-duplication repeatable.

Key idea

ROW_NUMBER, NTILE, LAG and LEAD need an ORDER BY that is unique within the partition, or tied rows can trade places from one run to the next, and every filter on a window result needs a CTE or derived table, because WHERE runs before the window.

Checkpoint

Check: De-duplicating with ROW_NUMBER

  1. Question 1 of 2 Order 101 has three rows, new at 09:00 (event 1), paid at 09:05 (event 2) and paid at 09:05 (event 3). Which row does ORDER BY updated_at DESC, event_id DESC number 1?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Event 3

    The DELETE through the CTE then removes events 1 and 2 and keeps event 3.

  2. Question 2 of 2 A CTE named numbered holds every status row with n = ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC, event_id DESC). Which statement after it removes the duplicates?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: DELETE FROM numbered WHERE n > 1;

    Run the same CTE with SELECT first to see the rows that the DELETE will remove.

Practise: number, read and filter the lab’s rows

The questions use the lab’s units, months and order feed. The idea you are least sure of comes up first, and every choice names the rule that makes it right or wrong:

Practice that adapts to you

Practise this lesson

13 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 What does DENSE_RANK() give the units 90, 20, 20 and 10, ordered highest first?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: 1, 2, 2, 3

    DENSE_RANK counts distinct values above the row, plus one.

  2. Question 2 of 13 Kochi has three products. Which group numbers does NTILE(2) OVER (PARTITION BY city ORDER BY units DESC, product) give them?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: 1, 1, 2

    Group sizes differ by one at most when the rows do not divide evenly.

  3. Question 3 of 13 A report keeps the rows with RANK() <= 3 for the units 90, 80, 80, 80 and 70. How many rows does it return?

    Type a number.

    Show the answer: Question 3 of 13

    Answer: 4

    The ranks are 1, 2, 2, 2 and 5, so four rows are ranked 3 or better and no row has rank 3 at all.

  4. Question 4 of 13 Which row does LAG(units, 2) OVER (PARTITION BY city ORDER BY month_no) read?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: The row two months before the current one in the same city

    For Pune's third month LAG(units, 2) reads month 1, 60.

  5. Question 5 of 13 What does LAG(units) OVER (PARTITION BY city ORDER BY month_no) return for Pune's first month?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: NULL

    A NULL here keeps a report honest: the first month has no previous month to compare with.

  6. Question 6 of 13 The query forgets PARTITION BY and writes LEAD(units) OVER (ORDER BY city, month_no). What does Kochi's last month get?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: Pune's first month, 60, because all rows now form one partition

    Ordered by city, Kochi's rows come first, so its last month is followed by Pune's first.

  7. Question 7 of 13 A dashboard shows units - LAG(units, 1, 0) OVER (PARTITION BY city ORDER BY month_no) as each month's change. What is wrong with Pune's first month?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: It shows a rise of 60, as if Pune had sold nothing the month before

    Leave the default out for changes and growth rates, so the first month shows NULL.

  8. Question 8 of 13 Can a SQL Server 2025 query filter on a window function with QUALIFY?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: No; Microsoft lists QUALIFY for Fabric Data Warehouse only, so SQL Server needs a CTE or derived table

    A port from an engine with QUALIFY moves the condition into the outer query of a CTE.

  9. Question 9 of 13 May a T-SQL query sort its result with ORDER BY ROW_NUMBER() OVER (PARTITION BY city ORDER BY units DESC)?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: Yes; ORDER BY is one of the two clauses where window functions are allowed

    Sorting by a window result works in place; filtering on one needs the CTE.

  10. Question 10 of 13 A delivery planner needs exactly two products per city, even when units tie. Which numbering in the CTE fits?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: ROW_NUMBER() OVER (PARTITION BY city ORDER BY units DESC, product), keeping n <= 2

    Exact counts need ROW_NUMBER; ties kept need RANK or DENSE_RANK.

  11. Question 11 of 13 In ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC), which row of each order gets the number 1?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: The newest one

    Keeping n = 1 keeps the newest status of each order.

  12. Question 12 of 13 The feed holds six status rows for three orders, and the de-duplication keeps one row per order. How many rows does the DELETE remove?

    Type a number.

    Show the answer: Question 12 of 13

    Answer: 3

    Three rows are numbered 1, one per order, and the other three are numbered 2 or 3 and deleted.

  13. Question 13 of 13 Two rows of order 101 share updated_at 09:05, and the ORDER BY has no tie-breaker. Which of them does the DELETE keep?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Either one; SQL Server does not promise which tied row gets the number 1

    Add a unique column, such as event_id, at the end of the ORDER BY to make the result repeatable.

Interview questions

Warm-up (fresher to 2–5 years): for the units 90, 20, 20 and 10, what do ROW_NUMBER, RANK and DENSE_RANK give?

ROW_NUMBER gives 1, 2, 3 and 4: every row its own number, with the order of the two 20s decided by the tie-breaker, or by chance when there is none. RANK gives 1, 2, 2 and 4: tied rows share a number, and the next row’s number counts the rows before it, which leaves a gap. DENSE_RANK gives 1, 2, 2 and 3: ties share a number and the next distinct value takes the next number. All three need an ORDER BY inside OVER.

Warm-up (fresher to 2–5 years): in which clauses may a T-SQL query use a window function?

Only in SELECT and ORDER BY. SQL Server computes window functions in the SELECT step of a query, after FROM, WHERE, GROUP BY and HAVING, so a window function in WHERE fails with error 4108. T-SQL has no QUALIFY clause either; to filter on a window result, compute it in a CTE or derived table and filter in the outer query.

Two longer questions follow, one on the latest row per key at scale and one on a duplicate clean-up that must run safely in production. Note your approach to each, then open it:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A dashboard refreshed every minute needs the latest status of each order from a table of 40 million status events. One version numbers all events with ROW_NUMBER in a CTE and keeps n = 1; another uses CROSS APPLY with SELECT TOP (1) ordered by updated_at DESC. Compare the two and choose, with the index each needs.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A nightly feed has left duplicate customer rows, same email, different load times. A developer wants to run DELETE through a CTE with ROW_NUMBER() OVER (PARTITION BY email ORDER BY loaded_at DESC) WHERE n > 1 on production tonight. What would you check or change before it runs, and how do you make it safe to repeat?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do ROW_NUMBER, RANK, DENSE_RANK and NTILE number a tie at second place?

    ROW_NUMBER gives 1, 2, 3, ties or not; RANK repeats the number and skips the next, 1, 2, 2, 4; DENSE_RANK repeats it without a gap, 1, 2, 2, 3; NTILE(n) cuts the partition into n groups whose sizes differ by one at most, the larger ones first. All four return bigint and all four require an ORDER BY in OVER.

  2. When does ROW_NUMBER promise the same numbers on every run?

    Only when the partition and ORDER BY columns together are unique. Otherwise tied rows can trade places between runs, which is why a tie-breaker column goes at the end of the ORDER BY.

  3. What are LAG's optional arguments, and what are their rules?

    The offset, 1 by default and never negative, and the default value returned when the offset points past the edge of the partition, NULL when none is given and type-compatible with the first argument. Neither LAG nor LEAD takes a frame, and each partition starts again.

  4. Why is LAG(units, 1, 0) the wrong thing to subtract in a change column?

    The first row of each partition has no row before it, so the default 0 makes it look like a rise from nothing, and a growth rate divided by it fails with error 8134. Leave the default out so the first row's change is NULL and the report handles it; a default fits only where 0 is a true value.

  5. Why can a WHERE clause not filter on a window function's result?

    SQL Server binds the query FROM, ON, JOIN, WHERE, GROUP BY, WITH CUBE or ROLLUP, HAVING, SELECT, DISTINCT, ORDER BY, TOP, and window functions are computed in the SELECT step, so WHERE runs before they have a value. Error 4108 says windowed functions can only appear in the SELECT or ORDER BY clauses.

  6. Does T-SQL have QUALIFY?

    No. Microsoft lists QUALIFY for Fabric Data Warehouse only, while Oracle AI Database 26ai has one. In SQL Server every filter on a window result goes through a CTE or a derived table: number the rows inside, filter in the outer query.

  7. Which numbering function should a top-2 per group use?

    It depends on ties. ROW_NUMBER returns exactly two rows per group and drops one of a tied pair; RANK keeps the tie and returns three; DENSE_RANK counts distinct amounts, so it can return three in every group. For a single group, TOP (2) WITH TIES ... ORDER BY does what RANK() <= 2 does.

  8. How do you remove duplicate rows for one key?

    Number each key's rows newest first with ROW_NUMBER() OVER (PARTITION BY key ORDER BY updated_at DESC, event_id DESC), then DELETE through the CTE where the number is above 1, with OUTPUT deleted. to list what went. The tie-breaker is what makes the result repeatable.

Key takeaways

  • ROW_NUMBER, RANK, DENSE_RANK and NTILE differ only on ties and groups: 1, 2, 3, 4 against 1, 2, 2, 4 and 1, 2, 2, 3 for a tie at second place, and groups of nearly equal size for NTILE.
  • LAG and LEAD read a row before or after the current one within its partition, with an offset of 1 and a default of NULL unless the call says otherwise.
  • Window functions may appear only in SELECT and ORDER BY, and T-SQL has no QUALIFY, so top-N per group and every other filter on a window result go through a CTE or a derived table.
  • De-duplication numbers each key’s rows with a unique ORDER BY, keeps row 1 and deletes the rest through the CTE.

Terms in this lesson

offset function
A window function that reads a value from another row of the partition at a given distance from the current row, such as LAG for an earlier row and LEAD for a later one.
ranking function
A window function that numbers the rows of each partition in the order of its OVER clause: ROW_NUMBER, RANK, DENSE_RANK and NTILE in T-SQL, which differ in how they number ties.
tie-breaker
An extra column at the end of an ORDER BY, such as a key, that makes the order unique, so that rows with equal values in the other columns always come in the same order.
window partition
The group of rows that PARTITION BY gives a window function to work on; numbering, offsets and frames start again in each partition, and without PARTITION BY the whole result is one partition.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.