🍋🍋大數據學習🍋🍋
🔥系列專欄: 👑哲學語錄: 用力所能及,改變世界。
💖如果覺得博主的文章還不錯的話,請點贊👍+收藏??+留言📝支持一下博主哦🤞
窗口函數使用技巧
窗口函數是 SQL 中處理復雜數據分析的強大工具,常用于排名、累計計算、同比環比等場景。以下從基礎語法、高頻函數、性能優化、典型場景四個維度總結核心技巧,并附示例代碼和避坑指南。
一、基礎語法與核心組件
1. 完整語法結構
sql
<窗口函數> OVER ([PARTITION BY 字段1, 字段2...] -- 分組依據[ORDER BY 字段1 [ASC/DESC], 字段2...] -- 排序規則[窗口子句] -- 可選,定義窗口范圍
)
-
窗口函數分類:
- 排名函數:
ROW_NUMBER()
、RANK()
、DENSE_RANK()
- 聚合函數:
SUM()
、AVG()
、COUNT()
、MAX()
、MIN()
- 偏移函數:
LAG()
、LEAD()
、FIRST_VALUE()
、LAST_VALUE()
- 排名函數:
-
執行順序:
- FROM → WHERE → GROUP BY → HAVING(生成基礎結果集)
- 窗口函數計算(基于基礎結果集)
- SELECT → ORDER BY → LIMIT(過濾和排序最終結果)
二、高頻窗口函數與典型場景
1. 排名函數:處理排序與去重
函數 | 說明 | 示例(按工資降序) |
---|---|---|
ROW_NUMBER() | 強制唯一排名(1,2,3...) | ROW_NUMBER() OVER (ORDER BY salary DESC) |
RANK() | 允許并列,跳過后續排名(1,1,3) | RANK() OVER (ORDER BY salary DESC) |
DENSE_RANK() | 允許并列,不跳過后續排名(1,1,2) | DENSE_RANK() OVER (ORDER BY salary DESC) |
典型場景:
-
Top N 篩選:查詢每個部門工資最高的 3 人。
WITH ranked_employees AS (SELECT *,ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnFROM employees ) SELECT * FROM ranked_employees WHERE rn <= 3;
-
去重:刪除表中重復記錄,保留主鍵最小的行。
WITH duplicates AS (SELECT *,ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rnFROM users ) DELETE FROM duplicates WHERE rn > 1;
2. 聚合函數:動態計算分組統計值
典型場景:
-
累計計算:計算每月累計銷售額。
SELECT month,sales_amount,SUM(sales_amount) OVER (ORDER BY month) AS cumulative_sales FROM monthly_sales;
-
移動平均:計算近 3 天的平均訂單量。
SELECT order_date,order_count,AVG(order_count) OVER (ORDER BY order_date RANGE BETWEEN 2 PRECEDING AND CURRENT ROW) AS 3_day_avg FROM daily_orders;
3. 偏移函數:獲取前后行數據
函數 | 說明 | 示例(獲取前一個月銷售額) |
---|---|---|
LAG(字段, N) | 當前行向前第 N 行的值 | LAG(sales, 1) OVER (ORDER BY month) |
LEAD(字段, N) | 當前行向后第 N 行的值 | LEAD(sales, 1) OVER (ORDER BY month) |
典型場景:
-
環比計算:
SELECT month,sales,LAG(sales, 1) OVER (ORDER BY month) AS prev_month_sales,ROUND((sales - LAG(sales, 1) OVER (ORDER BY month)) / LAG(sales, 1) OVER (ORDER BY month) * 100, 2) AS growth_rate FROM monthly_sales;
-
異常檢測:找出銷售額突然變化的月份。
SELECT month,sales,ABS(sales - LAG(sales, 1) OVER (ORDER BY month)) AS sales_change FROM monthly_sales WHERE ABS(sales - LAG(sales, 1) OVER (ORDER BY month)) > 1000; -- 變化超過 1000
三、窗口子句(Window Frame)高級技巧
1. 窗口范圍定義
-- 語法
<窗口函數> OVER (PARTITION BY ...ORDER BY ...[ROWS/RANGE BETWEEN <起始> AND <結束>]
)
- ROWS vs RANGE:
ROWS
:按物理行計數(無論值是否相同)。RANGE
:按邏輯值范圍(相同值視為同一行)。
示例:計算當前行及前兩行的累計值。
SUM(sales) OVER (ORDER BY monthROWS BETWEEN 2 PRECEDING AND CURRENT ROW
)
2. 常見窗口范圍寫法
窗口范圍 | 說明 |
---|---|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | 從組的第一行到當前行(累計值) |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING | 當前行 + 前一行 + 后一行(移動窗口) |
RANGE BETWEEN 7 PRECEDING AND CURRENT ROW | 當前值的 7 天前到當前(時間窗口) |
四、性能優化與避坑指南
1. 索引優化
-
為?
PARTITION BY
?和?ORDER BY
?字段創建復合索引:
?CREATE INDEX idx_dept_salary ON employees (dept_id, salary);
- 適用場景:頻繁按部門分組并按工資排序的查詢。
-
覆蓋索引:若查詢僅需窗口函數結果,可創建覆蓋索引避免回表。
CREATE INDEX idx_month_sales ON monthly_sales (month, sales);
2. 避免重復計算
- 用 CTE 緩存中間結果:
WITH ranked_data AS (SELECT *,ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnFROM employees ) SELECT * FROM ranked_data WHERE rn <= 3; -- 避免重復計算排名
3. 窗口函數 vs 自連接
- 窗口函數更高效:
-- 低效:自連接計算部門平均工資 SELECT e.emp_id,e.salary,d.avg_salary FROM employees e JOIN (SELECT dept_id, AVG(salary) AS avg_salary FROM employees GROUP BY dept_id ) d ON e.dept_id = d.dept_id;-- 高效:窗口函數直接計算 SELECT emp_id,dept_id,salary,AVG(salary) OVER (PARTITION BY dept_id) AS avg_salary FROM employees;
五、高頻面試題
問題 1:如何用窗口函數實現用戶留存分析?
答案示例:
WITH first_login AS (SELECT user_id,MIN(login_date) AS first_dateFROM user_loginGROUP BY user_id
),
login_days AS (SELECT fl.user_id,fl.first_date,ul.login_date,DATEDIFF(ul.login_date, fl.first_date) AS days_since_firstFROM first_login flJOIN user_login ul ON fl.user_id = ul.user_id
)
SELECT first_date,COUNT(DISTINCT user_id) AS new_users,SUM(CASE WHEN days_since_first = 1 THEN 1 ELSE 0 END) AS day1_retained,ROUND(SUM(CASE WHEN days_since_first = 1 THEN 1 ELSE 0 END) / COUNT(DISTINCT user_id) * 100, 2) AS day1_retention_rate
FROM login_days
GROUP BY first_date
ORDER BY first_date;
問題 2:窗口函數中?ORDER BY
?的作用是什么?
答案要點:
- 對聚合函數:
ORDER BY
?定義窗口范圍的排序規則(如累計值按時間遞增)。 - 對排名函數:
ORDER BY
?決定按哪個字段排名(如按工資降序排名)。 - 若省略?
ORDER BY
:窗口范圍默認為整個分區(所有行)。
問題 3:LAG()
?和?LEAD()
?的區別是什么?
答案要點:
LAG()
:獲取當前行之前的第 N 行數據(用于環比、歷史對比)。LEAD()
:獲取當前行之后的第 N 行數據(用于預測、提前預警)。
六、實戰建議
-
復雜查詢分步驟編寫:
-- 步驟 1:計算基礎數據 WITH base_data AS (SELECT user_id,order_date,ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS order_rankFROM orders ) -- 步驟 2:篩選首次訂單 SELECT * FROM base_data WHERE order_rank = 1;
-
結合業務場景選擇窗口函數:
- 排名需求:用?
ROW_NUMBER()
/RANK()
。 - 時間序列分析:用?
LAG()
/LEAD()
。 - 動態聚合:用?
SUM() OVER()
/AVG() OVER()
。
- 排名需求:用?
-
注意窗口范圍邊界:
-- 錯誤:未指定 ORDER BY,默認窗口為整個分區 SUM(sales) OVER (PARTITION BY month) -- 返回每月總和,而非累計值-- 正確:明確 ORDER BY,計算累計值 SUM(sales) OVER (PARTITION BY month ORDER BY day)
總結:窗口函數使用策略
場景 | 推薦函數 | 關鍵參數 |
---|---|---|
分組排名 | ROW_NUMBER() ,?RANK() | PARTITION BY ,?ORDER BY |
累計計算 | SUM() OVER() ,?AVG() OVER() | ORDER BY ,?ROWS BETWEEN |
時間序列對比 | LAG() ,?LEAD() | ORDER BY , 偏移量(如 1) |
移動窗口統計 | AVG() OVER() ,?MAX() OVER() | ORDER BY ,?ROWS/RANGE |
首尾值獲取 | FIRST_VALUE() ,?LAST_VALUE() | ORDER BY ,?ROWS BETWEEN |
通過靈活組合窗口函數、窗口子句和索引優化,可高效解決各類復雜數據分析需求,提升 SQL 查詢的表達能力和執行效率。
用簡單的話直接說明partition by 與group by 的主要區別
核心區別:
-
分組后結果行數:
- GROUP BY:合并分組,結果行數減少(每組1行)。
- PARTITION BY:保留原始行,結果行數不變(每組多行)。
-
功能用途:
- GROUP BY:用于聚合統計(如求和、計數)。
- PARTITION BY:用于窗口計算(如排名、累計值)。
一句話總結:
- GROUP BY?是“合并同類項”,生成新的匯總行;
- PARTITION BY?是“分組加特效”,在原數據上疊加計算結果。
在出一些相關的sql上的細節問題
以下是補充的?SQL 細節問題清單,聚焦高頻考點中的易錯點和細節差異,覆蓋?JOIN、聚合函數、NULL 處理、索引優化?等維度,附答案要點和示例代碼:
一、JOIN 細節問題
1. LEFT JOIN 與 INNER JOIN 的結果差異
問題:
兩張表?A (id: 1,2,3)
?和?B (id: 2,3,4)
,執行?A LEFT JOIN B ON A.id = B.id
?和?A INNER JOIN B ON A.id = B.id
?的結果分別是什么?
答案要點:
- LEFT JOIN:返回 A 的所有行(1,2,3),匹配不上的 B 字段填?
NULL
。SELECT A.id, B.id FROM A LEFT JOIN B ON A.id = B.id; -- 結果:(1,NULL), (2,2), (3,3)
- INNER JOIN:僅返回匹配的行(2,3)。
SELECT A.id, B.id FROM A INNER JOIN B ON A.id = B.id; -- 結果:(2,2), (3,3)
2. ON 與 WHERE 條件的執行順序
問題:
LEFT JOIN
?中,ON A.x = B.x AND B.y = 'value'
?和?WHERE B.y = 'value'
?的區別是什么?
答案要點:
- ON 條件:在連接時過濾右表(B),不影響左表(A)的保留。
SELECT A.id, B.y FROM A LEFT JOIN B ON A.id = B.id AND B.y = 'active'; -- 結果:A 的所有行,B 中不滿足 y='active' 的行填 NULL
- WHERE 條件:在連接后過濾整個結果集,可能導致左表行被剔除。
SELECT A.id, B.y FROM A LEFT JOIN B ON A.id = B.id WHERE B.y = 'active'; -- 結果:僅保留 B.y='active' 的行,等價于 INNER JOIN
二、聚合函數細節
3. COUNT 的不同用法
問題:
COUNT(*)
、COUNT(column)
、COUNT(DISTINCT column)
?的區別是什么?
答案要點:
- COUNT(*):統計所有行,包括?
NULL
?值。 - COUNT(column):統計 column 非?
NULL
?的行。 - COUNT(DISTINCT column):統計 column 非重復且非?
NULL
?的值。
示例:
CREATE TABLE test (id INT, name VARCHAR(10));
INSERT INTO test VALUES (1, NULL), (2, 'Alice'), (3, 'Alice');SELECT COUNT(*) AS count_all, -- 結果:3COUNT(name) AS count_name, -- 結果:2(排除 NULL)COUNT(DISTINCT name) AS count_distinct; -- 結果:1(去重后 'Alice')
4. 聚合函數與 NULL 的關系
問題:
SUM(column)
、AVG(column)
?遇到?NULL
?值如何處理?
答案要點:
- 聚合函數忽略?
NULL
:CREATE TABLE sales (amount INT); INSERT INTO sales VALUES (100), (NULL), (200);SELECT SUM(amount) AS total, -- 結果:300(忽略 NULL)AVG(amount) AS average; -- 結果:150(計算時排除 NULL)
- 若需包含?
NULL
:用?COALESCE
?轉換為 0。SELECT SUM(COALESCE(amount, 0)) AS total, -- 結果:300 + 0 = 300AVG(COALESCE(amount, 0)) AS average; -- 結果:(100+0+200)/3 = 100
三、NULL 處理細節
5. NULL 的比較規則
問題:
WHERE column = NULL
?和?WHERE column IS NULL
?的區別是什么?
答案要點:
=
?無法判斷?NULL
:SELECT * FROM users WHERE email = NULL; -- 永遠返回空結果
- 必須用?
IS NULL
?或?IS NOT NULL
:SELECT * FROM users WHERE email IS NULL; -- 正確
6. COALESCE 與 IFNULL 的區別
問題:
COALESCE(value1, value2)
?和?IFNULL(value1, value2)
?的區別是什么?
答案要點:
- COALESCE:支持多個參數,返回第一個非?
NULL
?值(標準 SQL)。COALESCE(NULL, 1, 2) -- 結果:1
- IFNULL:僅支持兩個參數(MySQL 特有)。
IFNULL(NULL, 1) -- 結果:1
四、索引優化細節
7. 復合索引的最左匹配原則
問題:
索引?(a, b, c)
?能加速哪些查詢?
答案要點:
- 可加速:
WHERE a = 1 -- 匹配 a WHERE a = 1 AND b = 2 -- 匹配 a,b WHERE a = 1 AND b = 2 AND c = 3 -- 匹配 a,b,c
- 無法加速:
WHERE b = 2 -- 未從 a 開始 WHERE a = 1 AND c = 3 -- 跳過 b
8. 索引失效的常見場景
問題:
哪些操作會導致索引失效?
答案要點:
- 對索引字段使用函數:
WHERE YEAR(create_time) = 2023 -- 索引失效
- 隱式類型轉換:
WHERE id = '123' -- 若 id 為 INT,觸發類型轉換
- 范圍查詢后的字段:
WHERE a > 10 AND b = 20 -- 索引 (a, b) 僅 a 生效
五、SQL 執行順序細節
9. SQL 子句的執行順序
問題:
寫出?SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT
?的執行順序。
答案要點:
- FROM?→ 2.?WHERE?→ 3.?GROUP BY?→ 4.?HAVING?→ 5.?SELECT?→ 6.?ORDER BY?→ 7.?LIMIT
10. 子查詢與 JOIN 的性能差異
問題:
何時使用子查詢,何時使用 JOIN?
答案要點:
- 子查詢:適用于單行 / 單列的結果(如標量子查詢)。
SELECT name FROM users WHERE id = (SELECT user_id FROM orders WHERE order_id = 1);
- JOIN:適用于多表關聯,性能通常更優。
SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE o.order_id = 1;
六、日期時間處理細節
11. 日期比較的常見陷阱
問題:
如何查詢?2023-01-01
?到?2023-01-31
?之間的數據?
答案要點:
- 正確寫法:
WHERE date_column >= '2023-01-01' AND date_column < '2023-02-01';
- 錯誤寫法:
WHERE date_column BETWEEN '2023-01-01' AND '2023-01-31'; -- 可能漏掉 23:59:59 的記錄
12. 不同數據庫的日期函數差異
問題:
如何獲取當前日期?
答案要點:
數據庫 | 函數 |
---|---|
MySQL | CURDATE() ?或?NOW() |
PostgreSQL | CURRENT_DATE ?或?NOW() |
SQL Server | GETDATE() |
七、字符串處理細節
13. 字符串拼接的語法差異
問題:
如何拼接字符串?'Hello'
?和?'World'
?
答案要點:
數據庫 | 語法 | ||
---|---|---|---|
MySQL | CONCAT('Hello', 'World') | ||
PostgreSQL | `'Hello' | 'World'` | |
SQL Server | 'Hello' + 'World' |
14. 字符串截取函數
問題:
如何截取字符串?'abcdef'
?的前 3 個字符?
答案要點:
數據庫 | 函數 |
---|---|
MySQL | SUBSTRING('abcdef', 1, 3) |
PostgreSQL | SUBSTRING('abcdef', 1, 3) |
SQL Server | SUBSTRING('abcdef', 1, 3) |
八、業務場景細節
15. 如何實現分頁查詢?
問題:
查詢第 10 頁(每頁 10 條)的數據。
答案要點:
- MySQL/PostgreSQL:
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 90; -- 第 10 頁
- 優化大分頁:
SELECT * FROM users WHERE id > 90 LIMIT 10; -- 通過主鍵定位,避免全表掃描
16. 如何設計自增主鍵?
問題:
自增主鍵 vs UUID 的適用場景?
答案要點:
- 自增主鍵:性能優,適合頻繁插入(如訂單表)。
CREATE TABLE orders (id BIGINT AUTO_INCREMENT PRIMARY KEY, ...);
- UUID:分布式場景,避免主鍵沖突(如用戶表)。
CREATE TABLE users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ...);
九、面試應答技巧
-
分步驟解釋復雜問題:
例如解釋窗口函數時:" 第一步,
PARTITION BY
?將數據分組;第二步,ORDER BY
?確定每組內的排序;第三步,窗口函數對每組數據進行計算。" -
主動對比不同方案:
例如優化慢查詢時:"我會先分析執行計劃,若發現全表掃描,優先添加索引;若索引已存在但仍慢,考慮是否需要拆分大查詢或使用覆蓋索引。"
-
結合業務場景舉例:
例如解釋反范式化時:"在電商訂單表中,我們冗余存儲了用戶姓名,因為訂單查詢時 90% 的場景需要展示用戶信息,這樣能減少 JOIN 操作,提升查詢速度。"
總結
SQL 面試的細節陷阱主要集中在:
- 邏輯差異:
LEFT JOIN
?vs?INNER JOIN
、ON
?vs?WHERE
、GROUP BY
?vs?PARTITION BY
。 - NULL 處理:
NULL
?不能用?=
?比較,聚合函數默認忽略?NULL
。 - 索引優化:復合索引的最左匹配、索引失效場景。
- 函數差異:不同數據庫的日期 / 字符串函數語法。