累计销售突破百万是哪一天?SQLSQL累计问题之金额累加的五种解法

在这里插入图片描述

累计/累加问题是数据分析师经常遇到需要处理的情况,比如根据二八法则,百分之二十的产品销售数占到总额的百分之八十,就需要先求数额累计。这个问题在Excel中实现很简单,但是如果要用SQL取数就没有那么容易。

下面用实际案例数据还原真实取数场景,帮助你在实战中理解如何实现累计取数的过程,总结思路。

需求:如果你是某网店的数据分析师,现在老板要求吸引老顾客,对老顾客开展一项回馈活动,要找出自开店以来使店铺销售额达到一百万那一天的所有顾客。注意:不能超过一百万,也就是找到的顾客订单金额与之前所有订单金额之和小于等于一百万。你该怎么做?

背景:数据来源于微软示例数据库,一家销售自行车制造公司的销售数据,分为网络销售FactInternetSales和经销商销售FactResellerSales两张表,其中网络销售订单数据60398行,经销商销售数据有60855行。此处我们简化问题,重点在问题的解决上,只用到FactInternetSales表。表中有包含价格orderdate和salesamount在内的共计26个字段,每个日期存在多个订单情况。

分析:这个需求目的就是考察累加问题,需要按照时间顺序将订单金额累加找到使得店铺销售额小于等于一百万的那一天订单。销售额累计求和是目标,日期是累加条件。每个日期存在多个订单情况,说明需要注意分组情况。有些初学者可能虽然知道是按照日期排序累加,但在写SQL的时候往往忘记而对销售额自身排序再累加,求出来的结果“牛头不对马嘴”,检查半天可能都不知道哪里出错。

如果大家看了我写的排名问题和连续问题的解法,应该知道从简单查询、聚合函数/窗口函数、自定义变量和自连接四种角度去尝试,绝大多数SQL都逃不过这四种解法。学习SQL跟学习其他技能一样,如果没头没脑的背诵、不加总结的大量做题,实际是无用功。最有效的办法是,先学会基础,知道工具怎么用;再学方法套路,理解每个方法思路、关键;最后才是大量练习去形成自己的经验。逻辑很重要,搞清楚每一步骤与前后的关联就知道该怎么用。

角度一:首先尝试一下只使用12个SQL保留字的简单查询是否可以能直接取到预期结果。往往使用最简单的元素才是最快的解决途径,不信你自己回顾你的一些经历,绞尽脑汁都没想出最后用最笨的办法给解决了。

解法一 简单查询

下面是简单查询的代码:

with t as (select orderdate,sum(salesamount) ss 
           from factinternetsales 
           group by orderdate)
select distinct a.orderdate
from t a, t b
where a.orderdate >= b.orderdate
group by a.orderdate
having sum(b.ss) <= 1000000
order by a.orderdate desc
limit 1;

这段代码差不多使用了所有基本保留字,除了使用了临时表,其他部分应该都好理解。使用临时表是因为每个日期都有订单,需要先将每个日期的订单金额求出避免造成group by分组累加,而不是我们预期效果;另外也是出于实际工程中优化的考虑,主SQL语句中有笛卡尔积,而原表有6万多条26个字段数据,如果做笛卡尔积数据量会很大,我们也不需要取出那么多数据,所以通过临时表只取需要的数据字段。这也是实际工作中优化SQL时需要知道的一个原则,只取需要的字段而不要多余的字段。

在临时表t基础上,做笛卡尔积,这样a表的每个字段都对应b表中每个字段,但是我们不需要全连接,只需要符合条件的。那什么才是符合条件的?还记得在排名问题(见文章““茴”字有几种写法?SQL排名问题之全局排名的四种解法“)中讲的自连接的思路吗?如果我们对于a表的每一行,只需要取出b表中大于它的记录,那么按照顺序,a表最小的值对应b表中所有值,因为b表所有值都都大于等于最小的那个值,这个逻辑很简单,然后对a表这个值所连接的b表的所有值求和得。接下来第二小的值,对应b表中除了最小值以外的所有行数据,将它们求和。你将两个求和的值相减就会发现差值就是最小值。以此类推,第三小的值,对应b表中除了最小值和第二小值以外的所有行数据的和与第二小的值求和的差也是第二小的值本身。这样一直求导最大值,就形成了累加的效果。累加不就是上个值之前的所有值的和再加上当前值嘛。下面两张图清楚展示这种效果,从上到下依次是原表数据、连接后数据、求和后数据:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

再回到代码中,having后的条件就是限定不得超过一百万,这种写法就省去了写子查询繁琐的步骤。大家感兴趣可以自己写一下不使用having怎么写,如果不知道可以关注公众号“二八Data”私信我。最后将所有累加值order by排个序,取累计值为一百万对应的日期就好了。

这种利用笛卡尔积的思路一旦明白对解决很多类问题很有效。用count计数就是排名问题和连续,用sum求和就是累加问题,用avg就是求移动平均问题。

思路二:既然上面使用了表和它自身的连接,自然想到使用自连接的方法。为什么要多此一举呢?这就是我在连续问题(见文章“连续60天畅销产品怎么找?SQL相邻问题之连续登录的四种解法”中讲到的自连接就是简单查询的更高级写法,它原理和简单查询一样,但是写法更抽象,优点就是执行效率比简单查询快很多。

解法二 自连接

理解简单查询的连接思路之后,自然而然想到用自连接方式实现。以下是代码:

with t as (select orderdate,sum(salesamount) ss from factinternetsales group by orderdate)
select distinct a.orderdate
from t a
where 1000000 >= (select sum(b.ss) from t b where a.orderdate >= b.orderdate)
order by a.orderdate desc
limit 1;

临时表还是和上一种解法一样,唯一不同的是把笛卡尔积换成了条件where子句,这段代码的核心是where子句里括号里的内容,它获取的是一个标量,对b表中大于等于a表的数值求和一次跟100000做比较,符合小于等于100000条件的就取出,这样的好处是一边连接一边计算,求出结果就停止,效率岂不是更快?如果单独理解自连接这种解法,你可能怎么也无法理解,但是通过简单查询的思路变换过来,你应该能明白了吧?

解法三 自连接2

既然我们使用到自连接,这里还有一种写法:

with t as (select orderdate,sum(salesamount) ss from factinternetsales group by orderdate),
     t2 as (select a.orderdate,(select sum(s2.ss) from t s2 where    s2.orderdate<=a.orderdate) as cumsum from t a)
select orderdate 
from t2 
where cumsum <=1000000
order by t2.orderdate desc
limit 1;

这种写法将自连接写到select语句中去了,显得更加的复杂不好理解。实际上无论是执行效率还是代码量都不如第二种,但这也是一种思路,供参考。

思路三:使用聚合函数和窗口函数

解法四 聚合函数和窗口函数

实际上这个需求的解决考虑到解法困难程度和执行效率平衡来说,最适合的解法就是窗口函数,这也是为什么数据库厂商开发出窗口函数的功能,窗口函数的数量不多,但是使用可扩展性很强。下面是代码:

select distinct orderdate from 
(select orderdate,sum(salesamount) over(order by orderdate asc) ss from factinternetsales)a 
where a.ss<=1000000
order by a.orderdate desc
limit 1;

这短代码少了临时表,按照日期排序后直接求和,也只是取出需要的字段,并且少了一次求和。核心的地方就是sum和over怎么理解,之前我一直将聚合函数和窗口函数分开,其实over才是真正的窗口,和sum位置的函数一起组成分析函数,只不过某些在sum位置的函数被称呼习惯了就默认为窗口函数,而因为sum、count本身有名称所以才会有区分。

在over后的括号内有三个参数按顺序依次是partition by、order by 、rows between。partition by表示分组,就是将整个数据表按照某个条件分成一个个窗口,假如这里加上partition by orderdate,就意味着每个日期为一个组,对每个组内的销售额进行累计,而这里没有写说明不分窗口,整个表就是一个窗口,累加的范围就是整张表;order by orderdate asc表示按照日期排序,这也是我们的条件;rows between是在partition by分窗口之后再次分小窗口,详细用法之后会专门讲。在经过分好窗口排好序之后通过sum求和,它的原理是每一行的值都会计算从第一行到当前行的和,这个窗口是一个动态的,逻辑上不变的是从第一行到当前行,实际执行上会随着数据库扫描的位置窗口大小发生变化。

在这里插入图片描述

解法五 自定义变量

这种解法实际上是不推荐的,因为用户自定义变量是在MySQL窗口函数出来之前不得已而使用的一种解法。所以只要是能用窗口函数解决的都可以用自定义变量,但是它的效率注定是比窗口函数低的。这里为什么还要提供这种思路?因为还有很多问题是没有窗口函数可以解决的,那就只能使用这种笨办法,过时的方法不一定没有用。它也是一种思路。以下是代码:

with t as (select orderdate,sum(salesamount) ss from factinternetsales group by orderdate)
select orderdate        
from (select orderdate, @cum:=@cum+(@cur:=ss) as num    
      from t f1,(select @cum:=0,@cur:=0)init order by orderdate asc)s1 
      where num<=1000000 
      order by num desc 
      limit 1;

这里的关键是(select @cum:=0,@cur:=0) init这张临时表,说是一张表,其实就是一行数据,但是它和前面的f1表进行了笛卡尔积连接,这就组成了一张新表,在原来f1表每一行记录后面都增加了一行值就是@cum:=0和@cur:=0,这样就形成了新的两个辅助列的原型。如果在select后面接的不是@cum:=@cum+(@cur:=ss)而是直接接的是@cum:=0和@cur:=0这两个临时变量,那么它们各自这一列的值都是一样的都是0,但是我们使用临时变量的目的就是让它发生变化,每个新的累计值都在它前一个累计值基础上加上当前值,因此在这里@cum表示累加值,@cur表示当前值,“:="表示赋值,对于新的累加值@cum,将前一个累加值@cum与当前值赋值@cur给它,而当前是随着存储引擎一行一行扫描时不断变化的,所以我们将每一行销售额ss赋值给它。这样就达到累计的效果。用户自定义变量的精髓你学会了吗?还是学废了呢?再体会体会。

想彻底掌握SQL查询语句,而且是最快的办法,那就是拿着地图和指南针前进,地图告诉你要去哪二,指南针告诉你那是哪儿,地图就是SQL基础,指南针就是我这里四种通用思路,没有这两样听再多的SQL课程,做再多的题还是不会写SQL。

最后欢迎大家关注我,我是拾陆,搜索公众号“二八Data”,更多技术干货持续奉献。

告诉你要去哪二,指南针告诉你那是哪儿,地图就是SQL基础,指南针就是我这里四种通用思路,没有这两样听再多的SQL课程,做再多的题还是不会写SQL。

最后欢迎大家关注我,我是拾陆,搜索公众号“二八Data”,更多技术干货持续奉献。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lqw844597536/article/details/121432188