排名问题恐怕无论是公司面试还是日常工作都会经常遇到的问题。有很多初学者虽然学会了基本的SQL查询,也练习了一些SQL题目,但是始终不得SQL的要领。究竟怎么才能深刻掌握如何SQL取数呢?当然是会举一反三啦!举一隅不以三隅反,你就是没学会~
下面通过一个小小的例子来帮助你思考排名问题,希望对你有所帮助。而且我们使用的是公司实际订单数据而不是简单的几行数据,在接下来的例子中你会感受到几行数据的查询和几万行乃至上百千万的数据是不同的,如果你还停留在做题的思维而没有实战工程的思维,那么我可以说,少年你too young too simple啦
问题:怎么快速取出最高的价格?
背景:数据来源于微软示例数据库,一家自行车制造公司的销售数据,分为网络销售FactInternetSales和经销商销售FactResellerSales两张表,其中网络销售订单数据60398行,经销商销售数据有60855行。此处我们简化问题,重点在问题的解决上,因此只用到FactInternetSales表,它包含了价格UnitPrice在内的26个字段。
分析:数据工作中往往有两种需求,一种是未知的,这类问题难解决,没有人告诉你要取什么数,而是实际问题要求你取什么数,这需要数据分析师独立思考的能力;另一种是已知的,别人抛过来一个明确的需求,这类问题看似很难其实是很简单的。看得到的就有办法分析和解决,看不到的才是最可怕的。这里最重要的关键词是“价格”,限定条件一个是“最高的”,一个是“快速”,前者意味着我需要排序,后者意味着这个需求的解法不止一种,而我需要取出最好那一种。
初学者一上来可能就直接使用orderby和limit关键词,取出来就完了;又或者聪明点的会使用聚合函数、窗口函数蹭蹭蹭三下五去二了事儿。但实际上你只知其一不知其二,下次遇到相同问题变形就可能看不懂了。下面我由简到难介绍四种解法,给大家一个解决问题的思路。
解法一 简单查询
当遇到问题,最先想到的解决思路是什么?当然是用最简单的元素组合啦,在SQL中说的是最基本的关键词,在这道题也就是上面说的orderby和limit关键词。刚才我虽然批评这种解法并不是说它不能用,恰恰相反它是最应该优先使用的。为什么?黑猫白猫,抓住老鼠就是好猫。我们的目的是解决问题,这是根本所在,而找到最好的办法是放在第二位去考虑的。所以用这种方法取出预期的结果。
select distinct unitprice
from factinternetsales
order by unitprice desc
limit1;
解析一下这个答案:select关键字永远是写在第一个的,它后面跟的是我们要取的列,这里我们只需要取出“价格";取的数据是从哪里取?后面紧跟着的就是from+表名;order by 对数据进行排序,为什么要用desc呢?为什么不使用asc,然后取最后一条数据呢?这个思路是没毛病的,但是无论是人还是机器,我们在一个时间点只能处理一种执行顺序。打个比方,你站在桌子的这一头,需要从桌子上编有序号的一排水杯里取序号最大的那一个,现在你有个按钮可以让它们按顺序排好,你可以选择从大到小也可以选择从小到大排。但是你肯定只会决定让最大的那个号离你最近,而不是选择离你最远吧。你站在那里不动就可以使用最省力的方式取到水杯,肯定不会傻到走到对面去取。数据库也一样,它每一次执行都只会从头开始,而不会从尾部开始,如果你从小到大排,机器想要取出最后一个就只能走到最后去取,显然浪费了时间和空间。这个思维看似无用,也容易被人忽略,但它是后面比较难以理解的SQL自连接问题的基本思维;最后就是通过limit关键字限定1个来取出最高。整体思路很简单,只要是会写基本的SQL语句,知道SQL的书写顺序和执行顺序,大都可以写出。
最后还需要注意的点是结果集去重。在实际业务中往往会重复,我们只需要取一个,因此要用distinct关键字去重。在后面的所有的解法最后都会取出所有重复结果,也都需要去重的步骤。
解法二 聚合函数与窗口函数
SQL里除了有最基本的关键字之外还有许多可以提高效率的内置函数,对于对最大最小问题恰好有相应的函数可以使用。
#max
select
max(unitprice)
from
factinternetsales;
#first_value
select
distinct
first_value(unitprice) over()
from factinternetsales;
这个解法有两种,前者是用聚合函数,后者用窗口函数,在SQL中对排名问题恰好有max和min函数,还有很多其他业务问题都是没有对应的函数直接解决的;而窗口函数使用的范围就广一些。使用了max函数就可以不用写orderby和limit,因为它内部就已经做了相关处理。first_value关键字从字面意思来看就是取第一个值,它与max函数有两个不同,一个是无论想取出最大值还是最小值,只需要按相应的排序取第一个值就可以,也就是说一个关键字可以当两个使用,简单易用,而max函数要取最小值还得换成min函数;第二就是first_value是比max函数封装的更复杂的函数,从上面看到max函数实际上是对orderby和limit功能的封装,而first_value还考虑了将分组情况进行封装,相当于groupby之后再取max/min。在这里是一个简单的全局排序,表面看没有分组,其实全局就是一个巨大的组,所以我们也可以使用first_value。至于什么是窗口函数,我稍后会详细解释。这里简单解释一下就是:first_value(unitprice)表示取unitprice这一列的第一个值,over()表示窗口的大小,括号里没有内容默认是全局为一个窗口,并且窗口里默认排序方式为desc。这样一看,是不是窗口函数更加简洁?
解法三 用户自定义变量
为什么要使用用户自定义变量呢?因为对于这道排名问题,既然是排名,为什么不给每个结果编个号,排在第一的不就是最高的吗?就像我们的成绩表,在成绩后面加上一列表示排名,那么谁是最高的就一目了然了。这加上的一列辅助列没有在数据里,该怎么办呢?这时候就需要用到用户自定义变量,是mysql里的临时变量,仅在SQL语句中生效。
第一步:先增加排名辅助列
select unitprice,if(@u = unitprice,@r:=@r,@r:=@r+1), @u := unitprice
from factinternetsales f1,(select@u:=0,@r:=0) init
order by unitprice desc;
这里的关键是(select @u:=0,@r:=0) init这张临时表,说是一张表,其实就是一行数据,但是它和前面的f1表进行了笛卡尔积连接,这就组成了一张新表,在原来f1表每一行记录后面都增加了一个列值就是@u:=0和@r:=0,这样就形成了新的两个辅助列的原型。如果在select后面接的不是if语句而是直接接的是@u:=0和@r:=0这两个临时变量,那么它们各自这一列的值都是一样的都是0,但是我们使用临时变量的目的就是让它发生变化,相同的值相同的排名,不同的值增加排名。所以我们用if语句作出判断,当然需要先对价格进行降序排列,再进行一行一行的判断,从哪里开始判断呢,这就是解法一中说的SQL扫描方式,从第一行开始一行一行扫描直到所有数据遍历。第一行先取出最高的价格unitprice,if语句判断如果自定义变量@u = unitprice,则@r:=@r,在这里两个变量的分别代表着价格和排名,如果新列的价格@u等于价格列的价格,说明排名相同,如果不是@r就加1表示排名增加,这样就达到排名效果。需要解释的是:=表示的是赋值而不是等于号,可以给变量临时指定值。需要解释的是第三个@u := unitprice辅助列不能丢掉,如果丢掉就没有价格比较对象。
第二步:条件过滤最高
select distinct unitprice
from (selectunitprice,if(@u = unitprice, @r:=@r, @r:=@r +1) rk, @u:=unitprice
from factinternetsales f1, (select@u:=0, @r:=0) init
order by unitprice desc) a
where rk =1;
经过第一步,我们得到价格、排名、临时价格三个列,但是我们最后的结果只需要价格,那怎么办?只好将上一步得到的结果集作为子查询,再限定排名@r=1就可以得到预期结果了。
解法四 自连接
除了增加辅助列,还可以怎么办呢?还有一个思路就是既然取最高,那么数据里只有0个比他大;如果取第二,那么数据里只有1个比他大,以此类推,最小的那个数有n-1个比它大。那我们为什么不将这张表和它自身做一个关联。我们知道笛卡尔积是将两张表进行排列组合,A表的第一行会对应B表中的每一行,组成的新表行数为nA*nB。但是进一步,将筛选条件改为对A表的每一行都只取出B表中大于A的值,就会出现A表中最大值对应0个B表中的值,A表中第二大值对应1个B表中的值,以此类推达到我们想要的效果;最后取1个就得到最终结果。
select distinct unitprice from t f1
where1= (select count(*)
from t f2
where f2.unitprice >= f1.unitprice);
但是正如上面解释的,对于几行数据用笛卡尔积可以很快解决,但是对于上万行数据求笛卡尔积就会取数失败。原因就是取出的结果集数据量太大。所以这里实际工程采用公共临时表作为中继,产生一个数据量远小于原表但结果一样的数据集,这样就很快能提取结果。
with t as
(select distinct unitprice
from factinternetsales)
select distinct unitprice from t f1
where1= (select count(*)
fromt f2 where f2.unitprice >= f1.unitprice);
以上的解题思路,大家可以看到,其实都是一步一步拆分小问题得到的,在实际工作中总是会遇到各种各样的小问题,不是一个简简单单的答案能够帮你解决的。你从书本中学到的叫知识,只有把它运用出来才叫经验,知识是无用的,经验才是宝贵的;当你能够把你的经验举一反三解决同一类问题,这叫能力;更进一步地,如果你学会了这种解决问题的思维,能够无师自通的解决另一类问题,那就是潜力。潜力无限,机会才会无穷!
最后欢迎大家关注我,我是拾陆,搜索公众号“二八Data”,更多技术干货持续奉献。