文章目录
环境
- MySQL 8.0.28
- CentOS 7.9
问题
表 t1 如下:
| id | rec_time | score |
|---|---|---|
| 1 | 1767196800 | 1000 |
| 2 | 1767283200 | 1200 |
| 3 | 1767369600 | 0 |
| 4 | 1767456000 | 1100 |
| 5 | 1767542400 | 0 |
现在要查找"最近一次有积分的记录",如果所有记录都没有积分,则返回最近一次记录。
注:假设 rec_time 都是有效的时间戳, score 都是有效值(不存在负数或者null值)。
分析
方法1
获取所有的数据,然后从应用端查找符合条件的记录。
sql
select * from t1 where score > 0 order by rec_time desc
应用端处理数据时,可以先按score是否大于0来分组,再分别查找(先查score大于0的分组,若为空再查score等于0的分组)。也可以直接遍历结果集来查找,其逻辑大致如下:
令target为空,然后遍历结果集:
- 如果当前记录score值大于0,则这就是要查找的记录,令target为当前记录,break循环
- 否则,如果target为空,则令target为当前记录(最近一次score等于0的记录),否则continue循环(不是最近一次score等于0的记录)
总结:性能低下( t1 表可能数据量很大,DB服务器、网络数据传输、应用端解析的成本都很高),逻辑复杂。
方法2
分两步走。第一步先查找最近一次有积分的记录:
sql
select * from t1 where score > 0 order by rec_time desc limit 1
如果找到了,OK。
如果返回的结果集是空(没有有积分的记录),第二步,再查找最近一次记录:
sql
select * from t1 order by rec_time desc limit 1
注:第二步的SQL不需要加上 where score = 0 (当然加上也OK)。
总结:需要访问两次数据库。
方法3
在方法2的基础上,加以改进:通过 union all ,一次性获取这两条记录(并筛选其中的一条):
sql
(select * from t1 where score > 0 order by rec_time desc limit 1)
union all
(select * from t1 order by rec_time desc limit 1)
order by score desc limit 1
注意:括号是必需的,否则会报错(全局的 order by 、 limit 都是作用于全局的)。
注意:不要依赖隐式排序,最好还是加上 order by ,确保万无一失。
注意:第二个子查询没有加 where score = 0 ,这是OK的(其实加上更好理解)。
总结:挺好的,不过需要访问两次 t1 表。
方法4
直接遍历一次 t1 表就能获取目标记录:
sql
select * from t1
order by case when score > 0 then 1 else 2 end , rec_time desc
limit 1
如果看不明白,参考下面的SQL:
sql
select t1.*, case when score > 0 then 1 else 2 end as seq from t1;
结果如下:
| id | rec_time | score | seq |
|---|---|---|---|
| 1 | 1767196800 | 1000 | 1 |
| 2 | 1767283200 | 1200 | 1 |
| 3 | 1767369600 | 0 | 2 |
| 4 | 1767456000 | 1100 | 1 |
| 5 | 1767542400 | 0 | 2 |
也就是说,多加一列 seq ,有积分的记录值是1,没积分的记录值是2。
在此基础上,只需加上:
sql
order by seq, rec_time desc
limit 1
- 排序的第一个字段,把有积分的记录排在前面,没积分的记录排在后面
- 排序的第二个字段,按时间逆序
最后再取第一条记录。
这就达到目的了。
因此,完整的SQL是:
sql
select t1.*, case when score > 0 then 1 else 2 end as seq from t1
order by seq, rec_time desc
limit 1;
注意:我记得Db2不支持这种写法,因为 seq 不是一个字段,所以必须写成 order by case when score > 0 then 1 else 2 end 。
返回的结果集中,并不需要 seq 值,所以,上面的SQL可以转化为:
sql
select * from t1
order by case when score > 0 then 1 else 2 end , rec_time desc
limit 1
也就是最终的SQL。
注意,不能写成:
sql
select * from t1
order by score desc, rec_time desc
limit 1;
这样就变成获取最高积分的那条记录了。
关键点在于,"有积分"和"没积分"是两大类,每一类里面的记录,其积分是"一视同仁"的,差别只在于时间有前后。因此,通过 case ,把score设置"1"和"2"两个值,达到了"分类"效果。
总结:最佳方法。
其它
看另一个例子:假设表 t2 如下:
| id | stu_id | course | score |
|---|---|---|---|
| 1 | 1 | 语文 | 90 |
| 2 | 1 | 数学 | 80 |
| 3 | 1 | 英语 | 85 |
| 4 | 2 | 语文 | 77 |
| 5 | 2 | 数学 | 88 |
要统计参加了三科或以上考试的学生的平均分数:
sql
select stu_id, avg(score) from t2
group by stu_id
having count(*) >= 3
本例是过滤,所以更容易一些。事实上这就是最基础、最标准的写法。
和前面的例子类似,也可以写成:
sql
select stu_id, avg(score), count(*) as amount from t2
group by stu_id
having amount >= 3
这样更容易理解。
注意:我记得Db2不支持这种写法(因为 amount 不是字段),必须写成 having count(*) >= 3 才行。
如果不需要返回amount,就可以直接用前一种写法了。