[{"data":1,"prerenderedAt":169},["ShallowReactive",2],{"qa-\u002Fsql\u002Fwindow-functions\u002Franking-functions":3},{"page":4,"siblings":156,"blog":166},{"id":5,"title":6,"body":7,"description":61,"difficulty":65,"extension":66,"framework":67,"frameworkSlug":68,"meta":69,"navigation":70,"order":62,"path":71,"questions":72,"questionsCount":147,"related":148,"seo":149,"seoDescription":150,"stem":151,"subtopic":6,"topic":152,"topicSlug":153,"updated":154,"__hash__":155},"qa\u002Fsql\u002Fwindow-functions\u002Franking-functions.md","Ranking Functions",{"type":8,"value":9,"toc":60},"minimark",[10,15],[11,12,14],"h2",{"id":13},"about-ranking-functions","About Ranking Functions",[16,17,18,19,23,24,23,27,23,30,23,33,36,37,40,41,45,46,23,49,23,52,55,56,59],"p",{},"Ranking functions — ",[20,21,22],"code",{},"ROW_NUMBER",", ",[20,25,26],{},"RANK",[20,28,29],{},"DENSE_RANK",[20,31,32],{},"NTILE",[20,34,35],{},"PERCENT_RANK",",\n",[20,38,39],{},"CUME_DIST"," — assign a position to each row within its partition. The most-tested\ndistinction is how each handles ",[42,43,44],"strong",{},"ties"," (unique vs gaps vs no-gaps), which drives the\nright choice for ",[42,47,48],{},"top-N-per-group",[42,50,51],{},"deduplication",[42,53,54],{},"pagination",", and ",[42,57,58],{},"Nth\nhighest value"," problems — all built on the rank-in-a-CTE-then-filter pattern.",{"title":61,"searchDepth":62,"depth":62,"links":63},"",2,[64],{"id":13,"depth":62,"text":14},"medium","md","SQL","sql",{},true,"\u002Fsql\u002Fwindow-functions\u002Franking-functions",[73,78,82,86,90,94,99,103,107,111,115,119,123,127,131,135,139,143],{"id":74,"difficulty":75,"q":76,"a":77},"what-are-ranking-functions","easy","What are ranking window functions?","**Ranking functions** assign a position number to each row **within its\npartition**, based on the window's `ORDER BY`. The main ones are `ROW_NUMBER`,\n`RANK`, `DENSE_RANK`, and `NTILE`, plus the distribution functions `PERCENT_RANK`\nand `CUME_DIST`.\n\n```sql\nSELECT name, salary,\n       ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn,\n       RANK()       OVER (ORDER BY salary DESC) AS rnk,\n       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk\nFROM   employees;\n```\n\nAll ranking functions **require** an `ORDER BY` in the `OVER` clause — without an\norder, \"rank\" has no meaning.\n\nRule of thumb: ranking functions number rows by an ordering; they always need\n`ORDER BY` in `OVER`.\n",{"id":79,"difficulty":75,"q":80,"a":81},"row-number","What does ROW_NUMBER() do?","`ROW_NUMBER()` assigns a **unique, sequential integer** to each row within the\npartition, in the window's `ORDER BY` order — `1, 2, 3, ...` with **no ties and no\ngaps**. Even rows with equal ordering values get distinct numbers (the tie-break is\narbitrary unless you add more `ORDER BY` columns).\n\n```sql\nSELECT name, dept_id,\n       ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn\nFROM   employees;\n```\n\nIt's the go-to for **pagination**, **deduplication**, and **top-N-per-group**.\n\nRule of thumb: `ROW_NUMBER()` = a unique 1,2,3 sequence per partition, no ties —\nuse it when you need exactly one row per position.\n",{"id":83,"difficulty":65,"q":84,"a":85},"rank-vs-dense-rank","What is the difference between RANK() and DENSE_RANK()?","Both give **tied rows the same rank**, but they differ in what comes next:\n\n- `RANK()` **leaves gaps** after ties — if two rows tie at 1, the next is 3.\n- `DENSE_RANK()` **leaves no gaps** — after a tie at 1, the next is 2.\n\n```sql\n-- salaries: 100, 100, 90\n-- RANK():       1,   1,   3\n-- DENSE_RANK(): 1,   1,   2\nSELECT name, salary,\n       RANK()       OVER (ORDER BY salary DESC) AS rnk,\n       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk\nFROM employees;\n```\n\nRule of thumb: `RANK` skips numbers after ties (like Olympic ranking);\n`DENSE_RANK` keeps them consecutive.\n",{"id":87,"difficulty":65,"q":88,"a":89},"row-number-vs-rank","How does ROW_NUMBER() differ from RANK() and DENSE_RANK()?","The key difference is **how ties are handled**:\n\n- `ROW_NUMBER()` — always unique; tied rows get **different** numbers (arbitrary\n  order among the tie).\n- `RANK()` — tied rows get the **same** rank, then a **gap**.\n- `DENSE_RANK()` — tied rows get the **same** rank, **no gap**.\n\n```sql\n-- values 100, 100, 90:\n-- ROW_NUMBER: 1, 2, 3\n-- RANK:       1, 1, 3\n-- DENSE_RANK: 1, 1, 2\n```\n\nRule of thumb: choose `ROW_NUMBER` for unique positions, `RANK`\u002F`DENSE_RANK` when\nties should share a position (gaps vs no gaps).\n",{"id":91,"difficulty":65,"q":92,"a":93},"ntile","What does NTILE() do?","`NTILE(n)` divides the ordered rows of a partition into **n roughly equal buckets**\nand labels each row with its bucket number `1..n`. It's used for **quartiles,\ndeciles, percentile bands**, and bucketing.\n\n```sql\n-- split employees into 4 salary quartiles\nSELECT name, salary,\n       NTILE(4) OVER (ORDER BY salary DESC) AS quartile\nFROM   employees;\n```\n\nIf the row count doesn't divide evenly, the **earlier buckets get one extra row**.\n\nRule of thumb: `NTILE(n)` splits ordered rows into n balanced groups — use it for\nquartiles\u002Fdeciles and even distribution.\n",{"id":95,"difficulty":96,"q":97,"a":98},"percent-rank","hard","What does PERCENT_RANK() compute?","`PERCENT_RANK()` returns the **relative rank** of a row as a value between `0` and\n`1`: `(rank - 1) \u002F (total_rows - 1)`. The first row is always `0`; the last is `1`.\nIt tells you what fraction of rows rank **below** the current one.\n\n```sql\nSELECT name, salary,\n       PERCENT_RANK() OVER (ORDER BY salary) AS pct_rank\nFROM   employees;\n```\n\nIt's useful for **percentile-style comparisons** (e.g. \"this salary is higher than\n80% of others\").\n\nRule of thumb: `PERCENT_RANK()` = where a row sits on a 0–1 scale relative to the\nrest; first row 0, last row 1.\n",{"id":100,"difficulty":96,"q":101,"a":102},"cume-dist","What does CUME_DIST() compute and how does it differ from PERCENT_RANK()?","`CUME_DIST()` (cumulative distribution) returns the **fraction of rows with a value\nless than or equal to** the current row: `rows_\u003C=_current \u002F total_rows`. It ranges\nin `(0, 1]`.\n\nThe difference from `PERCENT_RANK()`:\n- `CUME_DIST` = count of rows **≤ current** \u002F total (includes the current row).\n- `PERCENT_RANK` = `(rank - 1) \u002F (n - 1)` (excludes current; first row is 0).\n\n```sql\nSELECT name, salary,\n       CUME_DIST()    OVER (ORDER BY salary) AS cume,\n       PERCENT_RANK() OVER (ORDER BY salary) AS pct_rank\nFROM employees;\n```\n\nRule of thumb: `CUME_DIST` answers \"what proportion are at or below me?\";\n`PERCENT_RANK` answers \"what's my relative rank position 0–1?\".\n",{"id":104,"difficulty":75,"q":105,"a":106},"ranking-requires-order-by","Why do ranking functions require an ORDER BY in the OVER clause?","Ranking is meaningless without a defined order — the function needs to know **by\nwhat** to rank. So `ROW_NUMBER`, `RANK`, `DENSE_RANK`, `NTILE`, etc. all **require**\n`ORDER BY` inside `OVER`; omitting it is an error in most databases.\n\n```sql\n-- ERROR: no ordering to rank by\nROW_NUMBER() OVER (PARTITION BY dept_id)\n\n-- correct\nROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC)\n```\n\n(Aggregate windows like `SUM OVER ()` don't need `ORDER BY`, but ranking functions\ndo.)\n\nRule of thumb: every ranking function needs `ORDER BY` in `OVER` to define the\nranking criterion.\n",{"id":108,"difficulty":65,"q":109,"a":110},"top-n-per-group","How do you select the top N rows per group?","The classic pattern: number rows within each partition with a ranking function in a\n**CTE\u002Fsubquery**, then filter on that number in the outer query (window functions\ncan't go in `WHERE`).\n\n```sql\n-- top 3 highest-paid employees per department\nWITH ranked AS (\n    SELECT name, dept_id, salary,\n           DENSE_RANK() OVER (PARTITION BY dept_id\n                              ORDER BY salary DESC) AS rnk\n    FROM employees\n)\nSELECT name, dept_id, salary\nFROM   ranked\nWHERE  rnk \u003C= 3;\n```\n\nUse `ROW_NUMBER` for \"exactly N rows\" or `RANK`\u002F`DENSE_RANK` to **include ties** at\nthe cutoff.\n\nRule of thumb: rank in a CTE, filter `rnk \u003C= N` — pick `ROW_NUMBER` for an exact N,\n`DENSE_RANK` to keep ties.\n",{"id":112,"difficulty":65,"q":113,"a":114},"nth-highest-value","How do you find the Nth highest value using ranking functions?","Rank the rows descending, then filter for the Nth rank. Use `DENSE_RANK` when you\nwant the Nth **distinct** value (so duplicate values count once).\n\n```sql\n-- the 3rd highest distinct salary\nWITH ranked AS (\n    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk\n    FROM employees\n)\nSELECT DISTINCT salary FROM ranked WHERE rnk = 3;\n```\n\nWith `ROW_NUMBER` you'd get the 3rd row, not the 3rd distinct value; with `RANK`\nyou'd risk gaps. `DENSE_RANK` is the safe choice for \"Nth highest distinct.\"\n\nRule of thumb: for the Nth highest distinct value, `DENSE_RANK() ... = N`.\n",{"id":116,"difficulty":65,"q":117,"a":118},"deduplication-with-row-number","How do you remove duplicate rows using ROW_NUMBER()?","Partition by the columns that define a duplicate, number the rows, and keep only\n`rn = 1` (deleting or excluding the rest).\n\n```sql\n-- keep the most recent record per email, drop older duplicates\nWITH ranked AS (\n    SELECT id, ROW_NUMBER() OVER (PARTITION BY email\n                                  ORDER BY created_at DESC) AS rn\n    FROM users\n)\nDELETE FROM users\nWHERE id IN (SELECT id FROM ranked WHERE rn > 1);\n```\n\nThe `ORDER BY` decides **which** duplicate is the \"keeper\" (e.g. newest).\n\nRule of thumb: `ROW_NUMBER()` partitioned by the dup key, keep `rn = 1`, remove\n`rn > 1` — the standard dedup pattern.\n",{"id":120,"difficulty":65,"q":121,"a":122},"pagination-with-row-number","How can ROW_NUMBER() be used for pagination?","Number the ordered rows, then select the slice for a page. This was the classic\npagination method before `OFFSET`\u002F`FETCH` and is still used in SQL Server pre-2012.\n\n```sql\n-- page 2, 10 rows per page (rows 11–20)\nWITH ordered AS (\n    SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn\n    FROM products\n)\nSELECT * FROM ordered WHERE rn BETWEEN 11 AND 20;\n```\n\nModern engines often prefer `ORDER BY ... LIMIT 10 OFFSET 10`, but `ROW_NUMBER`\npagination works everywhere and pairs well with deterministic ordering. Note both\nget slow at deep offsets — keyset pagination scales better.\n\nRule of thumb: `ROW_NUMBER` + `BETWEEN` slices pages; for large datasets prefer\nkeyset pagination over deep offsets.\n",{"id":124,"difficulty":65,"q":125,"a":126},"ranking-tie-breaking","How do you make ROW_NUMBER() deterministic when ordering values tie?","`ROW_NUMBER()` always produces unique numbers, but when the `ORDER BY` values tie,\nthe assignment **among tied rows is arbitrary** and can change between runs. Add a\n**tie-breaker** column (ideally a unique key) to the `ORDER BY` to make it\ndeterministic.\n\n```sql\nROW_NUMBER() OVER (ORDER BY salary DESC, id ASC)  -- id breaks ties stably\n```\n\nWithout the tie-break, paginating or deduplicating can return inconsistent results\nacross executions.\n\nRule of thumb: append a unique column to the window `ORDER BY` so tied rows get a\nstable, repeatable order.\n",{"id":128,"difficulty":75,"q":129,"a":130},"rank-with-partition","How does PARTITION BY affect ranking functions?","`PARTITION BY` makes the rank **restart at 1 for each group**. Without it, ranking\nruns across the entire result set as one partition.\n\n```sql\n-- rank salaries WITHIN each department (resets per dept)\nSELECT name, dept_id, salary,\n       RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dept_rank\nFROM   employees;\n```\n\nSo an employee can be rank 1 in their department even if they're not the\nhighest-paid company-wide.\n\nRule of thumb: add `PARTITION BY` to rank within groups (rank resets per group);\nomit it to rank globally.\n",{"id":132,"difficulty":65,"q":133,"a":134},"filtering-on-rank","Why can't you filter directly on a ranking function in WHERE?","Window functions — including ranking functions — are evaluated **after** `WHERE`,\nso the rank doesn't exist yet when `WHERE` runs. Referencing it there is an error.\n\n```sql\n-- ILLEGAL\nSELECT name FROM employees\nWHERE RANK() OVER (ORDER BY salary DESC) \u003C= 5;\n\n-- LEGAL: compute in a CTE, filter outside\nWITH r AS (\n    SELECT name, RANK() OVER (ORDER BY salary DESC) AS rnk FROM employees\n)\nSELECT name FROM r WHERE rnk \u003C= 5;\n```\n\nRule of thumb: ranks are computed after filtering — always wrap them in a CTE or\nsubquery before filtering.\n",{"id":136,"difficulty":96,"q":137,"a":138},"ntile-uneven-buckets","What happens with NTILE() when rows don't divide evenly?","When the row count isn't divisible by `n`, `NTILE` makes the **first buckets one\nrow larger** than the later ones. For example, 10 rows into `NTILE(3)` gives\nbuckets of sizes **4, 3, 3**.\n\n```sql\n-- 10 rows, NTILE(3): bucket 1 has 4 rows, buckets 2 and 3 have 3 each\nSELECT val, NTILE(3) OVER (ORDER BY val) AS bucket FROM nums;\n```\n\nThis guarantees buckets differ in size by at most one, with the extras front-loaded.\n\nRule of thumb: `NTILE` front-loads the remainder — earlier buckets get the extra\nrows when the count doesn't divide evenly.\n",{"id":140,"difficulty":65,"q":141,"a":142},"choosing-ranking-function","How do you choose between ROW_NUMBER, RANK, and DENSE_RANK?","Pick based on **how you want ties handled**:\n\n- Need **exactly one row** per position (pagination, dedup, \"the single latest\")\n  → `ROW_NUMBER()`.\n- Ties should **share a rank with gaps** (standings where 2 golds means no silver)\n  → `RANK()`.\n- Ties should **share a rank without gaps** (Nth distinct value, dense tiers)\n  → `DENSE_RANK()`.\n\n```sql\nROW_NUMBER() -- 1,2,3,4   (unique)\nRANK()       -- 1,1,3,4   (gap after tie)\nDENSE_RANK() -- 1,1,2,3   (no gap)\n```\n\nRule of thumb: unique → `ROW_NUMBER`; ties-with-gaps → `RANK`; ties-no-gaps →\n`DENSE_RANK`.\n",{"id":144,"difficulty":96,"q":145,"a":146},"median-with-percentile","How can ranking\u002Fdistribution functions help compute a median?","A median is the value at the 50th percentile. You can approximate it with\n`CUME_DIST`\u002F`PERCENT_RANK`, but most databases offer the dedicated **ordered-set\naggregate** `PERCENTILE_CONT`\u002F`PERCENTILE_DISC` (a `WITHIN GROUP` function, related\nto window analytics).\n\n```sql\n-- exact continuous median per department (PostgreSQL \u002F Oracle \u002F SQL Server)\nSELECT dept_id,\n       PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median_salary\nFROM   employees\nGROUP  BY dept_id;\n```\n\n`PERCENTILE_CONT` interpolates between rows; `PERCENTILE_DISC` returns an actual\ndata value.\n\nRule of thumb: for a median, prefer `PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY\n...)` over hand-rolling it from rank functions.\n",18,null,{"description":61},"SQL ranking function interview questions — ROW_NUMBER vs RANK vs DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST, top-N-per-group, and deduplication patterns.","sql\u002Fwindow-functions\u002Franking-functions","Window Functions","window-functions","2026-06-20","XP3lJpDpG6os_ZieA-3c5KBo1W1xOy7nVJY4ZYgYt8g",[157,161,162],{"subtopic":158,"path":159,"order":160},"Window Function Basics","\u002Fsql\u002Fwindow-functions\u002Fwindow-basics",1,{"subtopic":6,"path":71,"order":62},{"subtopic":163,"path":164,"order":165},"Frames & Offset Functions","\u002Fsql\u002Fwindow-functions\u002Fframes-and-offsets",3,{"path":167,"title":168},"\u002Fblog\u002Fsql-ranking-functions-row-number-rank","SQL Ranking Functions — ROW_NUMBER, RANK, DENSE_RANK, and NTILE",1782244107032]