在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,就可以直接用前一种写法了。

相关推荐
你想考研啊2 小时前
win11安装mysql
数据库·mysql
Gary董2 小时前
mysql全面优化从哪几方面入手
数据库·mysql
陌上丨2 小时前
深入理解Redis线程模型
数据库·redis·缓存
2501_948120152 小时前
数据库分布式锁在并发控制中的应用
数据库·分布式
自己的九又四分之三站台2 小时前
PGVector 详解:PostgreSQL 世界里的向量能力插件
数据库·postgresql
Humbunklung2 小时前
记一次MySQL数据库备份与SQL格式内容导出导入
数据库·sql·mysql
无限码力2 小时前
华为OD技术面真题 - 数据库Redis - 2
数据库·redis·华为od·面试真题·华为od技术面真题·华为od技术面八股文·华为od高频面试真题
xuekai200809012 小时前
Oracle 19C 最简单快速安装方式
数据库·oracle
码农水水2 小时前
小红书Java面试被问:mTLS(双向TLS)的证书验证和握手过程
java·开发语言·数据库·redis·python·面试·开源