在order by里优化SQL

文章目录

环境

  • 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 bylimit 都是作用于全局的)。

注意:不要依赖隐式排序,最好还是加上 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,就可以直接用前一种写法了。

相关推荐
末央&20 小时前
【天机论坛】项目环境搭建和数据库设计
java·数据库
徒 花20 小时前
数据库知识复习07
数据库·作业
素玥20 小时前
实训5 python连接mysql数据库
数据库·python·mysql
jnrjian20 小时前
text index 查看index column index定义 index 刷新频率 index视图
数据库·oracle
瀚高PG实验室21 小时前
审计策略修改
网络·数据库·瀚高数据库
言慢行善21 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
韶博雅21 小时前
emcc24ai
开发语言·数据库·python
有想法的py工程师21 小时前
PostgreSQL 分区表排序优化:Append Sort 优化为 Merge Append
大数据·数据库·postgresql
迷枫7121 天前
达梦数据库的体系架构
数据库·oracle·架构
夜晚打字声1 天前
9(九)Jmeter如何连接数据库
数据库·jmeter·oracle