|
| 1 | +--- |
| 2 | +title: 窗口函数ROW_NUMBER |
| 3 | +createTime: 2026/01/04 19:16:02 |
| 4 | +permalink: /article/nawr706a/ |
| 5 | +tags: |
| 6 | + - 数据库 |
| 7 | + - MySQL |
| 8 | +--- |
| 9 | +ROW_NUMBER() OVER实战:优雅实现分组取最新记录,一行代码搞定学生最新成绩查询。 |
| 10 | + |
| 11 | +<!-- more --> |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +作为一名班主任,你是不是经常需要查看每个学生最近一次考试的成绩?今天要介绍的SQL窗口函数,就能帮你轻松解决这个问题! |
| 16 | + |
| 17 | +## 真实的教学场景 |
| 18 | + |
| 19 | +假设我们是一所学校的教务系统管理员,需要为每个班级的**每个学生**获取他们**最近一次考试的成绩**。 |
| 20 | + |
| 21 | +我们有三个主要数据表: |
| 22 | +1. **学生表**:记录学生基本信息 |
| 23 | +2. **考试成绩表**:记录每次考试的成绩 |
| 24 | +3. **班级信息表**:记录班级状态 |
| 25 | + |
| 26 | +## 数据表结构(简化) |
| 27 | + |
| 28 | +```sql |
| 29 | +-- 学生表 |
| 30 | +CREATE TABLE students ( |
| 31 | + id INT PRIMARY KEY, -- 学生ID |
| 32 | + student_no VARCHAR(20), -- 学号 |
| 33 | + class_id INT, -- 班级ID |
| 34 | + student_name VARCHAR(50) -- 学生姓名 |
| 35 | +); |
| 36 | + |
| 37 | +-- 考试成绩表 |
| 38 | +CREATE TABLE exam_scores ( |
| 39 | + id INT PRIMARY KEY, -- 记录ID |
| 40 | + student_id INT, -- 学生ID |
| 41 | + exam_date DATE, -- 考试日期 |
| 42 | + subject VARCHAR(50), -- 科目 |
| 43 | + score INT, -- 分数 |
| 44 | + update_time DATETIME -- 更新时间 |
| 45 | +); |
| 46 | + |
| 47 | +-- 班级表 |
| 48 | +CREATE TABLE classes ( |
| 49 | + id INT PRIMARY KEY, -- 班级ID |
| 50 | + class_name VARCHAR(50), -- 班级名称 |
| 51 | + is_active TINYINT(1) -- 是否活跃 |
| 52 | +); |
| 53 | +``` |
| 54 | + |
| 55 | +## 解决方案:窗口函数 |
| 56 | + |
| 57 | +```sql |
| 58 | +-- 获取每个学生最近一次考试的成绩 |
| 59 | +SELECT student_id, student_no, class_name, subject, score, exam_date |
| 60 | +FROM ( |
| 61 | + SELECT |
| 62 | + s.id AS student_id, |
| 63 | + s.student_no, |
| 64 | + s.student_name, |
| 65 | + c.class_name, |
| 66 | + es.subject, |
| 67 | + es.score, |
| 68 | + es.exam_date, |
| 69 | + es.update_time, |
| 70 | + -- 核心魔法在这里! |
| 71 | + ROW_NUMBER() OVER ( |
| 72 | + PARTITION BY s.id -- 按学生ID分组 |
| 73 | + ORDER BY es.exam_date DESC -- 按考试日期降序排列 |
| 74 | + ) AS row_num |
| 75 | + FROM students s |
| 76 | + INNER JOIN exam_scores es ON s.id = es.student_id |
| 77 | + LEFT JOIN classes c ON s.class_id = c.id AND c.is_active = 1 |
| 78 | + WHERE s.student_no IN ('2023001', '2023002', '2023003') |
| 79 | +) temp_table |
| 80 | +WHERE row_num = 1 -- 只取每个学生的第一条(最新的)记录 |
| 81 | +ORDER BY class_name, student_no; |
| 82 | +``` |
| 83 | + |
| 84 | +## 逐步拆解这个"魔法" |
| 85 | + |
| 86 | +### 第一步:理解数据 |
| 87 | +假设学生张三的考试记录: |
| 88 | +``` |
| 89 | +ID: 2023001, 张三的考试记录: |
| 90 | +1. 2023-09-01 数学 85分 |
| 91 | +2. 2023-10-08 数学 90分 ← 最新的 |
| 92 | +3. 2023-08-20 数学 78分 |
| 93 | +``` |
| 94 | + |
| 95 | +### 第二步:窗口函数执行过程 |
| 96 | + |
| 97 | +```sql |
| 98 | +ROW_NUMBER() OVER ( |
| 99 | + PARTITION BY s.id -- 对每个学生单独编号 |
| 100 | + ORDER BY es.exam_date DESC -- 按考试日期从新到旧排序 |
| 101 | +) AS row_num |
| 102 | +``` |
| 103 | + |
| 104 | +执行结果会是: |
| 105 | +``` |
| 106 | +学生2023001(张三): |
| 107 | +考试日期 科目 分数 row_num |
| 108 | +2023-10-08 数学 90 1 ← 最新的一次 |
| 109 | +2023-09-01 数学 85 2 |
| 110 | +2023-08-20 数学 78 3 |
| 111 | +
|
| 112 | +学生2023002(李四): |
| 113 | +考试日期 科目 分数 row_num |
| 114 | +2023-10-05 英语 92 1 ← 最新的一次 |
| 115 | +2023-09-02 英语 88 2 |
| 116 | +``` |
| 117 | + |
| 118 | +### 第三步:筛选最新记录 |
| 119 | +```sql |
| 120 | +WHERE row_num = 1 |
| 121 | +``` |
| 122 | +这样就只保留了每个学生最近的那次考试成绩。 |
| 123 | + |
| 124 | +## 实际应用场景 |
| 125 | + |
| 126 | +### 场景1:成绩单打印 |
| 127 | +```sql |
| 128 | +-- 打印高一一班所有学生最近一次数学考试成绩 |
| 129 | +WHERE c.class_name = '高一一班' |
| 130 | + AND es.subject = '数学' |
| 131 | + AND row_num = 1 |
| 132 | +``` |
| 133 | + |
| 134 | +### 场景2:进步奖评选 |
| 135 | +```sql |
| 136 | +-- 找出每个学生最近两次考试,计算进步情况 |
| 137 | +SELECT * FROM ( |
| 138 | + SELECT ..., |
| 139 | + ROW_NUMBER() OVER ( |
| 140 | + PARTITION BY student_id |
| 141 | + ORDER BY exam_date DESC |
| 142 | + ) AS row_num |
| 143 | + ... |
| 144 | +) WHERE row_num <= 2 -- 取最近两次考试 |
| 145 | +``` |
| 146 | + |
| 147 | +## 传统方法的对比 |
| 148 | + |
| 149 | +### 传统方法(子查询): |
| 150 | +```sql |
| 151 | +-- 复杂且效率低 |
| 152 | +SELECT s.*, es1.* |
| 153 | +FROM students s |
| 154 | +INNER JOIN exam_scores es1 ON s.id = es1.student_id |
| 155 | +INNER JOIN ( |
| 156 | + SELECT student_id, MAX(exam_date) as latest_date |
| 157 | + FROM exam_scores |
| 158 | + GROUP BY student_id |
| 159 | +) es2 ON es1.student_id = es2.student_id |
| 160 | + AND es1.exam_date = es2.latest_date |
| 161 | +``` |
| 162 | + |
| 163 | +### 窗口函数方法: |
| 164 | +- **代码简洁**:逻辑一目了然 |
| 165 | +- **性能更优**:通常执行效率更高 |
| 166 | +- **扩展性强**:轻松调整取第N条记录 |
| 167 | + |
| 168 | +## 窗口函数其他妙用 |
| 169 | + |
| 170 | +### 1. 排名功能 |
| 171 | +```sql |
| 172 | +-- 每个班级内按成绩排名 |
| 173 | +RANK() OVER ( |
| 174 | + PARTITION BY class_id |
| 175 | + ORDER BY score DESC |
| 176 | +) AS class_rank |
| 177 | +``` |
| 178 | + |
| 179 | +### 2. 计算平均值 |
| 180 | +```sql |
| 181 | +-- 计算每个学生与班级平均分的差距 |
| 182 | +AVG(score) OVER ( |
| 183 | + PARTITION BY class_id |
| 184 | +) AS class_avg_score |
| 185 | +``` |
| 186 | + |
| 187 | +### 3. 累计计算 |
| 188 | +```sql |
| 189 | +-- 计算每个学生成绩的累计和 |
| 190 | +SUM(score) OVER ( |
| 191 | + PARTITION BY student_id |
| 192 | + ORDER BY exam_date |
| 193 | +) AS cumulative_score |
| 194 | +``` |
| 195 | + |
| 196 | +## 最佳实践建议 |
| 197 | + |
| 198 | +1. **索引优化**:确保`exam_date`字段有索引 |
| 199 | +2. **分区字段**:选择合适的分区字段,避免数据倾斜 |
| 200 | +3. **排序字段**:使用有索引的字段排序提升性能 |
| 201 | +4. **结果验证**:先用小数据量测试,确保逻辑正确 |
| 202 | + |
| 203 | +## 总结 |
| 204 | + |
| 205 | +通过`ROW_NUMBER() OVER`窗口函数,我们可以轻松解决"每组取最新/最老记录"这类常见需求。就像老师快速找出每个学生最新成绩一样简单! |
| 206 | + |
| 207 | +**关键记住三点**: |
| 208 | +1. `PARTITION BY`:告诉SQL如何分组 |
| 209 | +2. `ORDER BY DESC`:告诉SQL如何排序(DESC取最新) |
| 210 | +3. `WHERE row_num = 1`:告诉SQL只要每组第一条 |
0 commit comments