[{"data":1,"prerenderedAt":439},["ShallowReactive",2],{"topic-sql-window-functions":3},{"framework":4,"topic":16,"subtopics":25},{"id":5,"description":6,"extension":7,"icon":8,"meta":9,"name":10,"order":11,"slug":12,"stem":13,"tier":14,"__hash__":15},"frameworks\u002Fframeworks\u002Fsql.yml","SQL interview questions on queries, joins and aggregation — essential for every backend, data and analytics interview.","yml","database",{},"SQL",4,"sql","frameworks\u002Fsql",1,"lpzsOj2p9p9W0Tctwc61nP-ulZAA80R5gJiyaZS6ZeI",{"id":17,"description":18,"extension":7,"frameworkSlug":12,"meta":19,"name":20,"order":21,"slug":22,"stem":23,"__hash__":24},"topics\u002Ftopics\u002Fsql-window-functions.yml","OVER and PARTITION BY, ranking, LAG\u002FLEAD and frame clauses — per-row analytics that keep every row instead of collapsing groups.",{},"Window Functions",3,"window-functions","topics\u002Fsql-window-functions","E2b0Zeo68tkWufM6NH3u9_XbLvySYGeWBd_uC4vzY98",[26,162,295],{"id":27,"title":28,"body":29,"description":70,"difficulty":74,"extension":75,"framework":10,"frameworkSlug":12,"meta":76,"navigation":77,"order":14,"path":78,"questions":79,"questionsCount":154,"related":155,"seo":156,"seoDescription":157,"stem":158,"subtopic":159,"topic":20,"topicSlug":22,"updated":160,"__hash__":161},"qa\u002Fsql\u002Fwindow-functions\u002Fwindow-basics.md","Window Basics",{"type":30,"value":31,"toc":69},"minimark",[32,37],[33,34,36],"h2",{"id":35},"about-window-function-basics","About Window Function Basics",[38,39,40,41,45,46,50,51,54,55,58,59,62,63,65,66,68],"p",{},"Window functions are SQL's analytics workhorse: they compute aggregates, rankings, and\nrow-to-row comparisons ",[42,43,44],"strong",{},"without collapsing rows",", via the ",[47,48,49],"code",{},"OVER"," clause and its\n",[47,52,53],{},"PARTITION BY","\u002F",[47,56,57],{},"ORDER BY"," parts. Interviews probe the core distinction from ",[47,60,61],{},"GROUP BY",",\nwhere windows are (and aren't) allowed, how ",[47,64,57],{}," inside ",[47,67,49],{}," turns an aggregate\ninto a running total, and the CTE pattern for filtering on a window result.",{"title":70,"searchDepth":71,"depth":71,"links":72},"",2,[73],{"id":35,"depth":71,"text":36},"medium","md",{},true,"\u002Fsql\u002Fwindow-functions\u002Fwindow-basics",[80,85,89,93,97,101,105,109,113,117,122,126,130,134,138,142,146,150],{"id":81,"difficulty":82,"q":83,"a":84},"what-is-a-window-function","easy","What is a window function in SQL?","A **window function** performs a calculation across a set of rows — the\n**window** — that are related to the current row, **without collapsing them into\none row**. Unlike a `GROUP BY` aggregate, every input row stays in the output, but\neach gets an extra computed value.\n\n```sql\n-- each employee row keeps its detail AND gets the dept average alongside it\nSELECT name, dept_id, salary,\n       AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg\nFROM   employees;\n```\n\nThe `OVER` clause is what makes a function a window function. Window functions are\nideal for **running totals, rankings, moving averages, and row comparisons**.\n\nRule of thumb: a window function adds a per-row analytic value while keeping every\nrow — think \"aggregate without losing the detail.\"\n",{"id":86,"difficulty":82,"q":87,"a":88},"over-clause","What does the OVER clause do?","The `OVER` clause **defines the window** — the set of rows a window function\noperates on for each row. It can contain three optional parts: `PARTITION BY`\n(split rows into groups), `ORDER BY` (order rows within the window), and a **frame\nclause** (`ROWS`\u002F`RANGE`, narrowing the window further).\n\n```sql\nSELECT name, salary,\n       SUM(salary) OVER (PARTITION BY dept_id ORDER BY hire_date) AS running_total\nFROM   employees;\n```\n\nAn **empty** `OVER ()` makes the window the **entire result set** — every row sees\nall rows.\n\nRule of thumb: `OVER` turns an ordinary function into a window function and\nspecifies which rows it looks at.\n",{"id":90,"difficulty":74,"q":91,"a":92},"window-vs-group-by","What is the difference between a window function and GROUP BY?","`GROUP BY` **collapses** rows — one output row per group. A window function\n**preserves** every row and attaches the aggregate alongside each one.\n\n```sql\n-- GROUP BY: one row per department\nSELECT dept_id, AVG(salary) FROM employees GROUP BY dept_id;\n\n-- window: every employee row, plus their department's average\nSELECT name, dept_id, salary,\n       AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg\nFROM employees;\n```\n\nThis is why you can compare a row to its group (e.g. `salary` vs `dept_avg`) with\na window function — impossible with a bare `GROUP BY` because the detail rows are\ngone.\n\nRule of thumb: `GROUP BY` summarizes into fewer rows; a window function annotates\neach row without reducing the count.\n",{"id":94,"difficulty":82,"q":95,"a":96},"partition-by","What does PARTITION BY do in a window function?","`PARTITION BY` **divides** the rows into independent groups (partitions); the\nwindow function restarts for each partition. It's the windowing analog of\n`GROUP BY`, but the rows aren't collapsed.\n\n```sql\n-- numbering restarts at 1 within each department\nSELECT name, dept_id,\n       ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn\nFROM   employees;\n```\n\nWithout `PARTITION BY`, the whole result set is one single partition.\n\nRule of thumb: `PARTITION BY` says \"compute this window function separately within\neach group.\"\n",{"id":98,"difficulty":74,"q":99,"a":100},"partition-by-vs-group-by","How does PARTITION BY differ from GROUP BY?","Both split rows into groups, but the **output shape** differs:\n\n- `GROUP BY` produces **one row per group** — it reduces the result.\n- `PARTITION BY` keeps **all rows**, computing the window function within each\n  group while leaving the detail intact.\n\n```sql\n-- 1 row per dept\nSELECT dept_id, COUNT(*) FROM employees GROUP BY dept_id;\n\n-- every row, with its dept's count attached\nSELECT name, dept_id, COUNT(*) OVER (PARTITION BY dept_id) AS dept_count\nFROM employees;\n```\n\nRule of thumb: same grouping idea, different result — `GROUP BY` collapses,\n`PARTITION BY` annotates.\n",{"id":102,"difficulty":74,"q":103,"a":104},"order-by-in-over","What is the role of ORDER BY inside the OVER clause?","`ORDER BY` inside `OVER` **orders the rows within each partition**, which matters\nfor two reasons:\n\n1. **Ranking functions** (`ROW_NUMBER`, `RANK`, `LAG`, `LEAD`) need an order to be\n   meaningful.\n2. For aggregates, adding `ORDER BY` switches the default frame to a **running**\n   (cumulative) calculation up to the current row.\n\n```sql\n-- without ORDER BY: same total for whole partition\nSUM(amount) OVER (PARTITION BY user_id)\n-- with ORDER BY: a running total up to each row\nSUM(amount) OVER (PARTITION BY user_id ORDER BY order_date)\n```\n\nIt is **independent** of the query's outer `ORDER BY`, which only sorts the final\noutput.\n\nRule of thumb: `ORDER BY` in `OVER` orders rows for the window calc (and triggers\nrunning aggregates); the outer `ORDER BY` sorts the result.\n",{"id":106,"difficulty":74,"q":107,"a":108},"running-total","How do you compute a running total with a window function?","Use an aggregate (`SUM`) with `OVER (... ORDER BY ...)`. The `ORDER BY` makes the\ndefault frame cumulative — rows from the start of the partition up to the current\nrow.\n\n```sql\nSELECT order_date, amount,\n       SUM(amount) OVER (ORDER BY order_date) AS running_total\nFROM   orders;\n```\n\nAdd `PARTITION BY` to reset the running total per group (e.g. per customer):\n\n```sql\nSUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date)\n```\n\nRule of thumb: `SUM(x) OVER (ORDER BY ...)` is the canonical running total; add\n`PARTITION BY` to restart it per group.\n",{"id":110,"difficulty":82,"q":111,"a":112},"window-aggregate-functions","Can regular aggregate functions be used as window functions?","Yes — `SUM`, `AVG`, `COUNT`, `MIN`, and `MAX` all work as window functions simply\nby adding an `OVER` clause. They then compute over the window instead of\ncollapsing rows.\n\n```sql\nSELECT name, salary, dept_id,\n       COUNT(*)   OVER (PARTITION BY dept_id) AS dept_headcount,\n       MAX(salary) OVER (PARTITION BY dept_id) AS dept_max,\n       salary - AVG(salary) OVER (PARTITION BY dept_id) AS diff_from_avg\nFROM   employees;\n```\n\nThe same function name behaves as a group aggregate (with `GROUP BY`) or a window\naggregate (with `OVER`) depending on context.\n\nRule of thumb: any aggregate becomes a window function just by adding `OVER` — no\n`GROUP BY` required.\n",{"id":114,"difficulty":74,"q":115,"a":116},"ranking-vs-aggregate-windows","What are the main categories of window functions?","Window functions fall into three families:\n\n- **Aggregate windows** — `SUM`, `AVG`, `COUNT`, `MIN`, `MAX` over a window.\n- **Ranking functions** — `ROW_NUMBER`, `RANK`, `DENSE_RANK`, `NTILE`,\n  `PERCENT_RANK`, `CUME_DIST` — position a row within its partition.\n- **Value \u002F offset functions** — `LAG`, `LEAD`, `FIRST_VALUE`, `LAST_VALUE`,\n  `NTH_VALUE` — pull a value from another row in the window.\n\n```sql\nROW_NUMBER() OVER (ORDER BY score DESC)        -- ranking\nLAG(price)   OVER (ORDER BY day)               -- offset\nAVG(price)   OVER (PARTITION BY product_id)    -- aggregate\n```\n\nRule of thumb: aggregate windows summarize, ranking functions order, offset\nfunctions reach to neighboring rows.\n",{"id":118,"difficulty":119,"q":120,"a":121},"where-window-functions-allowed","hard","In which clauses can window functions be used?","Window functions are only allowed in the **`SELECT` list** and the **`ORDER BY`**\nclause. They are **not allowed** in `WHERE`, `GROUP BY`, or `HAVING`, because\nwindows are evaluated **after** those clauses (after grouping and filtering).\n\n```sql\n-- ILLEGAL: window function in WHERE\nSELECT name FROM employees\nWHERE ROW_NUMBER() OVER (ORDER BY salary) \u003C= 5;   -- error\n\n-- LEGAL: compute in a subquery\u002FCTE, then filter\nSELECT name FROM (\n    SELECT name, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn\n    FROM employees\n) t WHERE rn \u003C= 5;\n```\n\nRule of thumb: you can't filter on a window function directly — wrap it in a CTE\nor subquery and filter the alias.\n",{"id":123,"difficulty":119,"q":124,"a":125},"logical-processing-order","At what point are window functions evaluated in query processing?","Window functions run **late** in logical query processing — after `FROM`, `WHERE`,\n`GROUP BY`, and `HAVING`, but **before** the final `ORDER BY`, `DISTINCT`, and\n`LIMIT`. That ordering explains two facts:\n\n1. You can't reference a window function in `WHERE`\u002F`HAVING` (they run earlier).\n2. A window function operates on the rows that **survived** `WHERE`\u002F`GROUP BY`.\n\n```sql\n-- the SUM window only sees rows passing the WHERE filter\nSELECT name, SUM(salary) OVER ()\nFROM   employees\nWHERE  active = true;     -- filter applied BEFORE the window\n```\n\nRule of thumb: filtering happens first, windows next, final sort\u002Flimit last — so\nwindows see filtered rows but can't be filtered themselves.\n",{"id":127,"difficulty":74,"q":128,"a":129},"named-window","What is a named window (the WINDOW clause)?","The `WINDOW` clause lets you **define a window once** and reuse it by name across\nmultiple functions, avoiding repetition. It sits after `HAVING` and before\n`ORDER BY`.\n\n```sql\nSELECT name, dept_id, salary,\n       RANK()      OVER w AS rnk,\n       AVG(salary) OVER w AS dept_avg\nFROM   employees\nWINDOW w AS (PARTITION BY dept_id ORDER BY salary DESC);\n```\n\nBoth functions share window `w`. This is cleaner than repeating the same\n`PARTITION BY ... ORDER BY ...` in every function. (Supported in PostgreSQL, MySQL\n8+, SQL Server has limited support.)\n\nRule of thumb: define a window once in the `WINDOW` clause when several functions\nshare it.\n",{"id":131,"difficulty":74,"q":132,"a":133},"distinct-with-window","Can you use DISTINCT inside a window function?","Generally **no** — most databases do not support `COUNT(DISTINCT ...) OVER (...)`.\n`DISTINCT` aggregation isn't allowed with the `OVER` clause in standard SQL,\nPostgreSQL, and SQL Server.\n\n```sql\n-- typically ERRORS\nCOUNT(DISTINCT customer_id) OVER (PARTITION BY region)\n\n-- workaround: DENSE_RANK over the values, take the max\nSELECT region,\n       MAX(DENSE_RANK() OVER (PARTITION BY region ORDER BY customer_id))\n           OVER (PARTITION BY region) AS distinct_customers\nFROM orders;\n```\n\nCommon workarounds are a `DENSE_RANK` trick, or pre-aggregating distinct values in\na CTE.\n\nRule of thumb: `COUNT(DISTINCT)` as a window function usually isn't allowed —\npre-aggregate or use a `DENSE_RANK` workaround.\n",{"id":135,"difficulty":119,"q":136,"a":137},"window-function-performance","What are the performance considerations of window functions?","Window functions require the engine to **sort or hash** rows by the\n`PARTITION BY`\u002F`ORDER BY` keys, which is the main cost. Tips:\n\n- **Index** the partition\u002Forder columns so the engine can avoid a separate sort.\n- Each distinct window definition may add its own sort — **share windows** (named\n  `WINDOW` clause) where possible.\n- Filter rows **before** the window (`WHERE`) to shrink the input.\n- Beware huge partitions and unbounded frames (`RANGE` with peers) — they scan\n  more rows per output row.\n\nRule of thumb: window functions trade a sort for analytics — index the\npartition\u002Forder keys and minimize the number of distinct windows.\n",{"id":139,"difficulty":74,"q":140,"a":141},"window-vs-self-join","Why are window functions preferred over self-joins for analytics?","Before window functions, computing running totals, rankings, or row-to-row\ncomparisons required **correlated subqueries or self-joins**, which are verbose and\noften O(n²). Window functions express the same logic in **one pass**, more\nreadably and usually faster.\n\n```sql\n-- old self-join running total (slow, O(n^2))\nSELECT a.day, SUM(b.amount) AS rt\nFROM sales a JOIN sales b ON b.day \u003C= a.day\nGROUP BY a.day;\n\n-- window function (one pass)\nSELECT day, SUM(amount) OVER (ORDER BY day) AS rt FROM sales;\n```\n\nRule of thumb: replace self-joins\u002Fcorrelated subqueries for running totals and\nrankings with window functions — clearer and faster.\n",{"id":143,"difficulty":119,"q":144,"a":145},"filter-clause-window","How can you conditionally aggregate within a window?","Use the `FILTER (WHERE ...)` clause (PostgreSQL\u002FSQLite) or a `CASE` expression\ninside the aggregate (portable) to aggregate only rows meeting a condition within\nthe window.\n\n```sql\n-- PostgreSQL FILTER\nSELECT region,\n       COUNT(*) FILTER (WHERE status = 'paid') OVER (PARTITION BY region) AS paid\nFROM orders;\n\n-- portable CASE equivalent\nSELECT region,\n       SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END)\n           OVER (PARTITION BY region) AS paid\nFROM orders;\n```\n\nRule of thumb: use `FILTER (WHERE ...)` where supported, or `SUM(CASE WHEN ...)`\nfor a portable conditional window aggregate.\n",{"id":147,"difficulty":82,"q":148,"a":149},"empty-over-clause","What does an empty OVER() clause mean?","An empty `OVER ()` makes the window the **entire result set** — every row sees all\nthe rows. It's used to attach a grand total or overall aggregate to each row\nwithout grouping.\n\n```sql\nSELECT name, salary,\n       salary * 100.0 \u002F SUM(salary) OVER () AS pct_of_total_payroll\nFROM   employees;\n```\n\nHere `SUM(salary) OVER ()` is the company-wide payroll, repeated on every row, so\nyou can compute each person's share.\n\nRule of thumb: `OVER ()` with no partition or order = the whole result set —\nperfect for \"share of total\" calculations.\n",{"id":151,"difficulty":74,"q":152,"a":153},"combining-window-with-where","How do you filter rows based on a window function's result?","Since window functions aren't allowed in `WHERE`, compute the window value in a\n**CTE or subquery**, then filter the alias in the outer query. This is the standard\npattern for \"top-N per group\" and deduplication.\n\n```sql\n-- keep only the highest-paid employee per department\nWITH ranked AS (\n    SELECT name, dept_id, salary,\n           ROW_NUMBER() OVER (PARTITION BY dept_id\n                              ORDER BY salary DESC) AS rn\n    FROM employees\n)\nSELECT name, dept_id, salary\nFROM   ranked\nWHERE  rn = 1;\n```\n\nRule of thumb: wrap the window function in a CTE, then filter its output column —\nyou can't put it in `WHERE` directly.\n",18,null,{"description":70},"SQL window function interview questions — OVER, PARTITION BY, window vs GROUP BY aggregates, running totals, named windows, and where windows can be used.","sql\u002Fwindow-functions\u002Fwindow-basics","Window Function Basics","2026-06-20","lU-Pdyj0JfKs5j1DsgcOY_HWoWio2OyoTkAgClSQdgI",{"id":163,"title":164,"body":165,"description":70,"difficulty":74,"extension":75,"framework":10,"frameworkSlug":12,"meta":216,"navigation":77,"order":71,"path":217,"questions":218,"questionsCount":154,"related":155,"seo":291,"seoDescription":292,"stem":293,"subtopic":164,"topic":20,"topicSlug":22,"updated":160,"__hash__":294},"qa\u002Fsql\u002Fwindow-functions\u002Franking-functions.md","Ranking Functions",{"type":30,"value":166,"toc":213},[167,171],[33,168,170],{"id":169},"about-ranking-functions","About Ranking Functions",[38,172,173,174,177,178,177,181,177,184,177,187,190,191,194,195,198,199,177,202,177,205,208,209,212],{},"Ranking functions — ",[47,175,176],{},"ROW_NUMBER",", ",[47,179,180],{},"RANK",[47,182,183],{},"DENSE_RANK",[47,185,186],{},"NTILE",[47,188,189],{},"PERCENT_RANK",",\n",[47,192,193],{},"CUME_DIST"," — assign a position to each row within its partition. The most-tested\ndistinction is how each handles ",[42,196,197],{},"ties"," (unique vs gaps vs no-gaps), which drives the\nright choice for ",[42,200,201],{},"top-N-per-group",[42,203,204],{},"deduplication",[42,206,207],{},"pagination",", and ",[42,210,211],{},"Nth\nhighest value"," problems — all built on the rank-in-a-CTE-then-filter pattern.",{"title":70,"searchDepth":71,"depth":71,"links":214},[215],{"id":169,"depth":71,"text":170},{},"\u002Fsql\u002Fwindow-functions\u002Franking-functions",[219,223,227,231,235,239,243,247,251,255,259,263,267,271,275,279,283,287],{"id":220,"difficulty":82,"q":221,"a":222},"what-are-ranking-functions","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":224,"difficulty":82,"q":225,"a":226},"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":228,"difficulty":74,"q":229,"a":230},"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":232,"difficulty":74,"q":233,"a":234},"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":236,"difficulty":74,"q":237,"a":238},"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":240,"difficulty":119,"q":241,"a":242},"percent-rank","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":244,"difficulty":119,"q":245,"a":246},"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":248,"difficulty":82,"q":249,"a":250},"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":252,"difficulty":74,"q":253,"a":254},"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":256,"difficulty":74,"q":257,"a":258},"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":260,"difficulty":74,"q":261,"a":262},"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":264,"difficulty":74,"q":265,"a":266},"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":268,"difficulty":74,"q":269,"a":270},"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":272,"difficulty":82,"q":273,"a":274},"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":276,"difficulty":74,"q":277,"a":278},"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":280,"difficulty":119,"q":281,"a":282},"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":284,"difficulty":74,"q":285,"a":286},"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":288,"difficulty":119,"q":289,"a":290},"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",{"description":70},"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","XP3lJpDpG6os_ZieA-3c5KBo1W1xOy7nVJY4ZYgYt8g",{"id":296,"title":297,"body":298,"description":70,"difficulty":119,"extension":75,"framework":10,"frameworkSlug":12,"meta":359,"navigation":77,"order":21,"path":360,"questions":361,"questionsCount":154,"related":155,"seo":434,"seoDescription":435,"stem":436,"subtopic":437,"topic":20,"topicSlug":22,"updated":160,"__hash__":438},"qa\u002Fsql\u002Fwindow-functions\u002Fframes-and-offsets.md","Frames And Offsets",{"type":30,"value":299,"toc":356},[300,304],[33,301,303],{"id":302},"about-frames-offset-functions","About Frames & Offset Functions",[38,305,306,307,54,310,313,314,177,317,190,320,177,323,177,326,329,330,338,339,342,343,346,347,351,352,355],{},"Frame clauses (",[47,308,309],{},"ROWS",[47,311,312],{},"RANGE BETWEEN ...",") and offset functions (",[47,315,316],{},"LAG",[47,318,319],{},"LEAD",[47,321,322],{},"FIRST_VALUE",[47,324,325],{},"LAST_VALUE",[47,327,328],{},"NTH_VALUE",") power SQL's most advanced analytics — moving\naverages, period-over-period deltas, and gaps-and-islands. The highest-value interview\ntraps are the ",[42,331,332,334,335],{},[47,333,309],{}," vs ",[47,336,337],{},"RANGE"," distinction, the ",[42,340,341],{},"default frame"," (",[47,344,345],{},"RANGE ... CURRENT ROW","), and the ",[42,348,349],{},[47,350,325],{}," gotcha that needs an explicit ",[47,353,354],{},"UNBOUNDED FOLLOWING"," frame.",{"title":70,"searchDepth":71,"depth":71,"links":357},[358],{"id":302,"depth":71,"text":303},{},"\u002Fsql\u002Fwindow-functions\u002Fframes-and-offsets",[362,366,370,374,378,382,386,390,394,398,402,406,410,414,418,422,426,430],{"id":363,"difficulty":74,"q":364,"a":365},"what-is-a-window-frame","What is a window frame?","A **window frame** narrows the window to a **subset of rows around the current\nrow**, within its partition. It's defined with a `ROWS` or `RANGE` clause inside\n`OVER`, and controls exactly which rows an aggregate like `SUM` or `AVG` sees.\n\n```sql\n-- 3-row moving average: current row + the two before it\nSELECT day, amount,\n       AVG(amount) OVER (ORDER BY day\n                         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg\nFROM   sales;\n```\n\nFrames enable **moving averages, running totals over a sliding window, and\nlookback\u002Flookahead aggregates**.\n\nRule of thumb: a frame is the sliding sub-window an aggregate operates on — set it\nwith `ROWS`\u002F`RANGE BETWEEN ... AND ...`.\n",{"id":367,"difficulty":119,"q":368,"a":369},"rows-vs-range","What is the difference between ROWS and RANGE in a frame?","Both define the frame boundaries, but they count differently:\n\n- `ROWS` works on **physical row positions** — \"the 2 rows before this one,\"\n  regardless of their values.\n- `RANGE` works on **value ranges** of the `ORDER BY` column — rows whose ordering\n  value falls within an offset, treating **ties (peers) as a unit**.\n\n```sql\n-- ROWS: exactly 3 physical rows\nSUM(x) OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)\n\n-- RANGE: all rows with the same day are included together\nSUM(x) OVER (ORDER BY day RANGE BETWEEN 2 PRECEDING AND CURRENT ROW)\n```\n\nWith duplicate `ORDER BY` values, `RANGE` includes **all** peer rows; `ROWS`\ncounts each physically.\n\nRule of thumb: `ROWS` = count physical rows; `RANGE` = include all rows with values\nin range (peers grouped).\n",{"id":371,"difficulty":119,"q":372,"a":373},"default-frame","What is the default window frame when you specify ORDER BY but no frame?","When you add `ORDER BY` to an aggregate window **without** an explicit frame, the\ndefault is `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. This gives a\n**running (cumulative)** aggregate — but because it's `RANGE`, **tied rows are\nincluded together** through the current row.\n\n```sql\n-- default frame = running total, but RANGE means peers are summed together\nSUM(amount) OVER (ORDER BY day)\n```\n\nThis trips people up: with duplicate `day` values, each tied row shows the **same**\ncumulative total (all peers included). Use `ROWS` for true row-by-row accumulation.\n\nRule of thumb: `ORDER BY` with no frame = `RANGE ... UNBOUNDED PRECEDING TO CURRENT\nROW`; switch to `ROWS` to avoid peer-grouping surprises.\n",{"id":375,"difficulty":74,"q":376,"a":377},"no-order-by-frame","What is the window frame when there is no ORDER BY?","With **no `ORDER BY`** in the `OVER` clause, the frame is the **entire partition** —\nevery row in the partition sees all of them. The aggregate is the same for all rows\nin that partition.\n\n```sql\n-- every row gets the department total (whole partition)\nSUM(salary) OVER (PARTITION BY dept_id) AS dept_total\n```\n\nThis is why `SUM(x) OVER (PARTITION BY g)` gives a group total on each row, while\nadding `ORDER BY` turns it into a running total.\n\nRule of thumb: no `ORDER BY` → frame is the full partition (a flat group aggregate);\nadding `ORDER BY` makes it cumulative.\n",{"id":379,"difficulty":119,"q":380,"a":381},"frame-boundaries","What are the possible frame boundary keywords?","A frame is `BETWEEN \u003Cstart> AND \u003Cend>`, where each bound is one of:\n\n- `UNBOUNDED PRECEDING` — the first row of the partition.\n- `n PRECEDING` — n rows (or values) before the current row.\n- `CURRENT ROW` — the current row (or its peers under `RANGE`).\n- `n FOLLOWING` — n rows (or values) after the current row.\n- `UNBOUNDED FOLLOWING` — the last row of the partition.\n\n```sql\n-- centered 3-row average: one before, current, one after\nAVG(x) OVER (ORDER BY day ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)\n\n-- whole partition\nSUM(x) OVER (ORDER BY day ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)\n```\n\nRule of thumb: combine `UNBOUNDED`\u002F`n PRECEDING`, `CURRENT ROW`, `n FOLLOWING`\u002F\n`UNBOUNDED` to frame any sliding or anchored window.\n",{"id":383,"difficulty":74,"q":384,"a":385},"moving-average","How do you compute a moving average?","Use `AVG` with a `ROWS` frame spanning the desired window of rows around the\ncurrent row.\n\n```sql\n-- 7-day moving average (current day + previous 6)\nSELECT day, amount,\n       AVG(amount) OVER (ORDER BY day\n                         ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7\nFROM   sales;\n```\n\nUse `ROWS` (not `RANGE`) for a fixed count of rows. For a **centered** average,\nframe `BETWEEN 3 PRECEDING AND 3 FOLLOWING`. Early rows average fewer rows unless\nyou handle warm-up.\n\nRule of thumb: moving average = `AVG(x) OVER (ORDER BY t ROWS BETWEEN n PRECEDING\nAND CURRENT ROW)`.\n",{"id":387,"difficulty":82,"q":388,"a":389},"lag-function","What does the LAG() function do?","`LAG(col, offset, default)` returns the value of `col` from a **previous row**\nwithin the partition — `offset` rows back (default 1). If there's no such row, it\nreturns `default` (or `NULL`). It's the classic tool for **comparing a row to the\nprior one**.\n\n```sql\n-- compare each day's sales to the previous day\nSELECT day, amount,\n       LAG(amount, 1, 0) OVER (ORDER BY day) AS prev_amount,\n       amount - LAG(amount) OVER (ORDER BY day) AS day_over_day\nFROM   sales;\n```\n\nRule of thumb: `LAG()` looks **backward** to the previous row — perfect for\nperiod-over-period deltas.\n",{"id":391,"difficulty":82,"q":392,"a":393},"lead-function","What does the LEAD() function do?","`LEAD(col, offset, default)` is the mirror of `LAG` — it returns a value from a\n**following row**, `offset` rows ahead (default 1), or `default`\u002F`NULL` past the\nend. Use it to look **forward**.\n\n```sql\n-- gap until the customer's next order\nSELECT customer_id, order_date,\n       LEAD(order_date) OVER (PARTITION BY customer_id\n                              ORDER BY order_date) AS next_order,\n       LEAD(order_date) OVER (PARTITION BY customer_id\n                              ORDER BY order_date) - order_date AS days_gap\nFROM   orders;\n```\n\nRule of thumb: `LEAD()` looks **forward** to the next row — use it for \"time until\nnext event\" or next-value comparisons.\n",{"id":395,"difficulty":74,"q":396,"a":397},"lag-lead-offset-default","What are the optional arguments of LAG() and LEAD()?","Both take three arguments: `LAG(expr [, offset [, default]])`.\n\n- `expr` — the column\u002Fexpression to fetch.\n- `offset` — how many rows back\u002Fforward (default `1`).\n- `default` — value returned when the offset falls outside the partition\n  (default `NULL`).\n\n```sql\n-- value 2 rows back; if none, use 0 instead of NULL\nLAG(amount, 2, 0) OVER (ORDER BY day)\n```\n\nSupplying a `default` is the clean way to avoid `NULL`s at partition edges (e.g.\nthe first row's \"previous\" value).\n\nRule of thumb: pass `offset` to jump multiple rows and `default` to replace the\nedge `NULL`s.\n",{"id":399,"difficulty":74,"q":400,"a":401},"first-value-last-value","What do FIRST_VALUE() and LAST_VALUE() return?","`FIRST_VALUE(col)` returns `col` from the **first row of the frame**;\n`LAST_VALUE(col)` from the **last row of the frame**. They're handy for putting a\npartition's boundary value on every row.\n\n```sql\nSELECT name, dept_id, salary,\n       FIRST_VALUE(name) OVER (PARTITION BY dept_id\n                               ORDER BY salary DESC) AS highest_paid\nFROM   employees;\n```\n\n**Gotcha:** with the default frame (`... CURRENT ROW`), `LAST_VALUE` returns the\ncurrent row, not the partition's true last — you must widen the frame (see next\nquestion).\n\nRule of thumb: `FIRST_VALUE`\u002F`LAST_VALUE` grab a frame boundary value — mind the\nframe, especially for `LAST_VALUE`.\n",{"id":403,"difficulty":119,"q":404,"a":405},"last-value-frame-gotcha","Why does LAST_VALUE() often return the current row instead of the last?","Because the default frame with `ORDER BY` is `RANGE BETWEEN UNBOUNDED PRECEDING AND\nCURRENT ROW` — the frame **ends at the current row**, so `LAST_VALUE` sees the\ncurrent row as the last. To get the partition's true last value, **extend the frame\nto `UNBOUNDED FOLLOWING`**.\n\n```sql\n-- WRONG: returns current row's value\nLAST_VALUE(salary) OVER (PARTITION BY dept_id ORDER BY salary)\n\n-- RIGHT: frame covers the whole partition\nLAST_VALUE(salary) OVER (PARTITION BY dept_id ORDER BY salary\n                         ROWS BETWEEN UNBOUNDED PRECEDING\n                                  AND UNBOUNDED FOLLOWING)\n```\n\nRule of thumb: always set an explicit `... UNBOUNDED FOLLOWING` frame with\n`LAST_VALUE`, or it just returns the current row.\n",{"id":407,"difficulty":74,"q":408,"a":409},"nth-value","What does NTH_VALUE() do?","`NTH_VALUE(col, n)` returns `col` from the **nth row of the frame** (1-based). Like\n`LAST_VALUE`, it's frame-sensitive, so widen the frame to see the whole partition.\n\n```sql\n-- the 2nd highest-paid employee's name, shown on every row of the dept\nSELECT name, dept_id,\n       NTH_VALUE(name, 2) OVER (PARTITION BY dept_id ORDER BY salary DESC\n                                ROWS BETWEEN UNBOUNDED PRECEDING\n                                         AND UNBOUNDED FOLLOWING) AS second_top\nFROM   employees;\n```\n\nRows before the nth position return `NULL`. (Supported in PostgreSQL, MySQL 8+;\nnot in SQL Server.)\n\nRule of thumb: `NTH_VALUE(col, n)` fetches the nth frame row — widen the frame to\ntarget the whole partition.\n",{"id":411,"difficulty":74,"q":412,"a":413},"running-total-vs-frame","How does the frame turn an aggregate into a running vs windowed total?","The frame decides what an aggregate accumulates:\n\n- **Running total** — `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`\n  (everything up to now).\n- **Sliding window total** — `ROWS BETWEEN n PRECEDING AND CURRENT ROW`\n  (last n+1 rows).\n- **Full partition total** — `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED\n  FOLLOWING` (or omit `ORDER BY`).\n\n```sql\nSUM(x) OVER (ORDER BY day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) -- running\nSUM(x) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)         -- last 7\n```\n\nRule of thumb: the frame's start bound sets the accumulation window — `UNBOUNDED\nPRECEDING` for running, `n PRECEDING` for sliding.\n",{"id":415,"difficulty":74,"q":416,"a":417},"lag-lead-vs-self-join","Why use LAG\u002FLEAD instead of a self-join for row comparisons?","Comparing each row to its neighbor with a **self-join** is verbose, needs a way to\ndefine \"previous\" (often a correlated subquery with `MAX(... \u003C current)`), and is\nslow. `LAG`\u002F`LEAD` do it in a **single ordered pass**, clearly and efficiently.\n\n```sql\n-- self-join approach (awkward, slower)\nSELECT a.day, a.amount - b.amount AS delta\nFROM sales a LEFT JOIN sales b ON b.day = a.day - 1;\n\n-- LAG approach (clean, one pass)\nSELECT day, amount - LAG(amount) OVER (ORDER BY day) AS delta FROM sales;\n```\n\nRule of thumb: prefer `LAG`\u002F`LEAD` over self-joins for previous\u002Fnext comparisons —\nsimpler and faster.\n",{"id":419,"difficulty":119,"q":420,"a":421},"gaps-and-islands","How do offset\u002Franking functions solve gaps-and-islands problems?","**Gaps-and-islands** problems (finding consecutive runs or missing ranges) are\nsolved by spotting where sequences break — using `LAG`\u002F`LEAD` to detect gaps, or the\n\"`ROW_NUMBER` difference\" trick to label islands.\n\n```sql\n-- group consecutive login days into \"islands\"\nWITH marked AS (\n    SELECT user_id, login_date,\n           login_date - (ROW_NUMBER() OVER (PARTITION BY user_id\n                                            ORDER BY login_date)) * INTERVAL '1 day'\n           AS grp\n    FROM logins\n)\nSELECT user_id, MIN(login_date) AS streak_start, MAX(login_date) AS streak_end,\n       COUNT(*) AS streak_len\nFROM marked GROUP BY user_id, grp;\n```\n\nRows in the same run share a constant `grp` value because the date and row number\nadvance together.\n\nRule of thumb: detect gaps with `LAG`\u002F`LEAD`; identify islands with the\ndate-minus-`ROW_NUMBER` constant-group trick.\n",{"id":423,"difficulty":119,"q":424,"a":425},"frame-with-range-interval","Can RANGE frames use intervals (e.g. time-based windows)?","Yes — `RANGE` frames can use a **value offset**, including time intervals, so the\nwindow is defined by the `ORDER BY` value rather than a row count. This gives true\ntime-based windows even when rows are irregularly spaced.\n\n```sql\n-- sum of sales in the trailing 7 days by date value (not row count)\nSELECT day, amount,\n       SUM(amount) OVER (ORDER BY day\n                         RANGE BETWEEN INTERVAL '7 days' PRECEDING\n                                   AND CURRENT ROW) AS rolling_7d\nFROM sales;\n```\n\nUnlike `ROWS`, this correctly handles missing days and multiple rows per day.\n(PostgreSQL and modern engines support `RANGE` with numeric\u002Finterval offsets.)\n\nRule of thumb: use `RANGE` with an interval\u002Fnumeric offset for value-based windows\n(e.g. \"last 7 days\") that don't depend on row counts.\n",{"id":427,"difficulty":74,"q":428,"a":429},"offset-functions-null-handling","How do LAG\u002FLEAD and FIRST_VALUE handle NULLs and partition edges?","At **partition edges**, `LAG`\u002F`LEAD` return their `default` argument (or `NULL`) —\ne.g. the first row has no previous row. They also return the **actual stored value**\neven if it's `NULL`; they don't skip `NULL` data values.\n\n```sql\n-- first row's prev is 0 (the default), not an error\nLAG(amount, 1, 0) OVER (ORDER BY day)\n```\n\nSome databases (Oracle, and via `IGNORE NULLS` in standard SQL \u002F newer engines)\nsupport `IGNORE NULLS` to skip `NULL` data values and fetch the nearest non-null.\n\nRule of thumb: edges yield the `default`\u002F`NULL`; supply a `default` to clean them\nup, and use `IGNORE NULLS` (where supported) to skip null data.\n",{"id":431,"difficulty":74,"q":432,"a":433},"combining-frames-with-partition","How do frames interact with PARTITION BY?","A frame is always **bounded by the partition** — it never crosses partition\nboundaries. `UNBOUNDED PRECEDING` means the first row **of the current partition**,\nand `LAG`\u002F`LEAD` stop at the partition edge.\n\n```sql\n-- running total resets at each customer; frame stays within the partition\nSELECT customer_id, order_date, amount,\n       SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date\n                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rt\nFROM orders;\n```\n\nSo per-customer running totals start fresh for each customer automatically.\n\nRule of thumb: frames live inside their partition — `UNBOUNDED` and offsets are\nrelative to the partition, not the whole table.\n",{"description":70},"SQL window frame interview questions — ROWS vs RANGE, frame boundaries, LAG\u002FLEAD, FIRST_VALUE\u002FLAST_VALUE, moving averages, and default frame gotchas.","sql\u002Fwindow-functions\u002Fframes-and-offsets","Frames & Offset Functions","3jC4V3JmOMKHMW1anUBJYQ68qbQi1ZbUGVcd0ktsBo4",1782244097940]