MySQL的书写顺序与执行逻辑?SQL条件过滤之否定筛选的五种解法教你搞懂它!

            数据分析写SQL取数回归到本质就是筛选或者过滤,对原始数据进行某种给定条件的筛选或者过滤,仔细思考一下表连接(join)不就是以另一张表为过滤条件、where后接的不就是过滤条件、group by分组不就是把分组情况作为条件?说到底就是按照条件的先后顺序一道一道把需要的数据筛选出来,因此条件过滤是SQL的基本技能,理解透了如何用join、如何用where、如何用groupby这三大筛选方式的原理,基本SQL就不在话下。

下面用实际案例数据还原真实取数场景,来从条件否定的角度帮助你在实战中理解如何实现SQL条件过滤取数的过程,总结思路和规律。

需求:写一条 SQL 语句找出在FactInternetSalesReason表中而不在DimSalesReason表中的SalesReasonkey和SalesReasonName,你该怎么做?

背景:数据来源于微软示例数据库,一家销售自行车制造公司的销售数据,公司为了改善产品,增进用户体验,需要了解顾客购买产品的消费理由,现在公司收集到两张表,一张FactInternetSalesReason表,包含订单号salesordernumber和SalesReasonkey在内三个字段,共计64515条记录,另一张表DimSalesReason记录每个销售因素,包含SalesReasonkey和SalesReasonName在内四个字段。因FactInternetSalesReason数据量大,现在先分组聚合后展示接下来将要操作的两张表的数据,让大家更清楚理解过程和结果:

分析:首先明确要取的数据字段是什么,需求中说了是FactInternetSalesReason表中的SalesReasonkey这一个字段,不过需要筛选,筛选出不在DimSalesReason表中的记录,这是典型的条件筛选,用where语句not in关键字筛选就可以了,还涉及到表连接(join)和分组(group by)。需求很明确,也很简单。但是我仍然提出五种思路供大家参考和动手实践,目的在于学会判断哪些思路写出的SQL是好的,这是SQL优化的第一步。

本篇是原创SQL解题总结系列第七篇,如果大家看了我之前的系列文章应该都知道SQL题大都有四个角度去考虑解法,但是对这道题只用到了第一种思路,就是简单查询(复合查询)。后面四种方法没有用到。为什么?因为在前面几篇里排名问题、连续问题、累加问题是数据纵向之间发生关系;两个均值比较、行转列问题涉及纵向和横向数据之间发生关系;但在简单的条件过滤中数据纵向之间和横向之间是没有发生关系的。再来看窗口函数、自定义变量、自连接都解决的是数据纵向之间关系,是在这里不适用的。这也给初学者提供写SQL的思路,究竟什么时候用窗口函数或自连接,什么时候不该用,是要判断取的数据要不要在纵向或横向发生关系。

简单说一下解题思路,主表是DimSalesReason,要取出的两个字段也都在这张表中,从表是FactInternetSalesReason,这里你就可以看到其实连接就是用从表筛选主表。那怎么筛选呢?下面从子查询、连接查询、联合查询三个角度来筛选,这也充分利用了所有SQL的基本知识,这也是我前面文章所说的一定理解基本SQL语句的功能,要实现某个目的可以创造条件让这个功能实现。下面直接给出全部解法,再一一对比解析。

解法一 子查询+not in

select SalesReasonkey,SalesReasonName from DimSalesReason 
where SalesReasonkey not in 
      (select SalesReasonkey 
       from (select SalesReasonkey,count(SalesReasonkey) 
             from FactInternetSalesReason 
             group by SalesReasonkey)a);
复制代码

解法二 子查询+not exists

select SalesReasonkey,SalesReasonName from DimSalesReason d
where not exists 
      (select SalesReasonkey 
       from (select SalesReasonkey,count(SalesReasonkey) 
             from FactInternetSalesReason 
             group by SalesReasonkey)a
       where d.SalesReasonkey = a.SalesReasonkey);
复制代码

解法三 连接查询+null

select d.SalesReasonkey,SalesReasonName 
from DimSalesReason d 
left join (select SalesReasonkey,count(SalesReasonkey) 
           from FactInternetSalesReason 
           group by SalesReasonkey)a
           on d.SalesReasonkey = a.SalesReasonkey
where a.SalesReasonkey is null;
复制代码

解法四 连接查询+isnull

select d.SalesReasonkey,SalesReasonName 
from DimSalesReason d 
left join (select SalesReasonkey,count(SalesReasonkey) 
           from FactInternetSalesReason 
           group by SalesReasonkey)a
           on d.SalesReasonkey = a.SalesReasonkey
where isnull(a.SalesReasonkey);
复制代码

解法五 联合查询

with t as 
(select SalesReasonkey,SalesReasonName 
 from DimSalesReason
  union
 select SalesReasonkey,count(SalesReasonkey) 
 from FactInternetSalesReason 
 group by SalesReasonkey
)
select SalesReasonkey,SalesReasonName 
from t 
group by SalesReasonkey 
having count(SalesReasonkey)=1;
复制代码

上面这五种解法的执行效率如下:

执行顺序.png

初学者应该都知道SQL的书写顺序和执行顺序不一样,下面这张图分别显示了是怎样的顺序:

可以看到SQL实际取数的过程是这样的:

步骤1.最开始执行的是from关键字,先去访问原数数据表,生成一张虚拟表t1(这张表包含原主表数据的全部数据,但是实际没有取数);

步骤2.如果有join关键字,就将join后面的表和t1表做个笛卡尔积,生成虚拟表t2(这张表包含原主表数据和从表的全部数据,但还是没有取数),如果没有就跳过,直接执行where以及之后语句;

步骤3.上一步有join关键字,则必须有on或者using关键字,否则语法上肯定报错。on的作用就相当于where,是筛选器功能。它表示的是两张表连接时在on后面条件下进行的,过滤掉不符合条件的生成虚拟表t3(依然没有实际取数),一般情况下是“=”连接条件,但是也可以接其他的,很多初学者可能错误的以为on后面只能接等于号;

步骤4.两张表连接好后经过on第一道筛选之后进入到where筛选器,生成虚拟表t4,where是几乎所有SQL必须要有的关键字,因为它是最靠前的筛选器,它可以为后面的执行过程提高效率;

步骤5.在上一步筛选基础上进行group by分组筛选,生成虚拟表t5。group by分组本质上就是where条件筛选再加上聚合,怎么理解呢?

上面可以理解为是一个先map(映射)再reduce(缩减),在主流数据库里使用group by 后必须使用聚合函数(reduce),因为要将每个组内复合条件的数据缩减为一行就需要判断或者计算,比如max/min/sum/count/avg等等,然后再将结果一个一个union(联合)起来;但是在MySQL里可以不用聚合,会默认取每组第一条数据,但是在sql server里是会报错的。

步骤6.在步骤5的基础上计算with cube或者rollup,这是对分组聚合的结果再汇总,就相当于excel表中求和时右下角对每组求和结果再求和的那个值。这里生成虚拟表t6。

步骤7.在步骤6基础上,对分组后的数据进行第三道筛选,它的作用也相当于where,注意必须与groupby搭配使用。这里生成虚拟表t7。

步骤8.在步骤7筛选完毕之后,就开始执行select语句实际取数,形成虚拟表t8,不过这里其实不是select第一次执行,它在from之前已经执行过一次,但是它的作用不是取数,而是搭建好后面取的数据的表结构和字段名,这也是为什么在having筛选条件里可以使用select语句中的字段别名。但是这仅限于MySQL,SqlServer、oracle是严格遵循SQL标准的。

步骤9.在步骤8的基础上对数据去重distinct,生成虚拟表t9。

步骤10.在步骤8的基础上对数据进行排序,排序是比筛选成本更高的操作,where条件筛选其实也是依据某种执行顺序,但是它是隐性的,order by是显性的。为了提高效率,我们往往需要在where和orderby后面的列增加索引,这样查询才会高效。这就是为什么要索引的原因。这一步生成虚拟表t9。

步骤11.这里用limit关键字对上一步的表进行截断,取得最终结果。

理解了上面全部执行顺序再来理解上面五种解法就很简单了。我可以把筛选条件放在on后面(连接查询),也可以放在where后面(子查询),也可以放在group by后面(联合查询)。在这个执行顺序过程中三道筛选,后一道都在前一道的基础上,说明前面所处理的表数据是大于后面的,如果我们在越靠前的位置尽可能多的过滤掉数据,那整体的执行效率就会大大提高。这就是上面显示为什么子查询和连接查询效率比联合查询高(理论上连接查询比子查询快,但因为数据量比较小,所以差不多,反而还慢)。

最后还需要解释的就是not in和not exists,null和isnull的用法和区别。

如果百度查in和exists的区别的话,大都说不建议使用in,但其实这需要因问题而定,要查看执行计划才能确定。因为涉及到全表扫描,数据内容不一样效率也会不一样。下面是区别:

1、对于not exists查询,内表存在空值对查询结果没有影响;对于not in查询,内表存在空值将导致最终的查询结果为空。

2、对于not exists查询,外表存在空值,存在空值的那条记录最终会输出;对于not in查询,外表存在空值,存在空值的那条记录最终将被过滤,其他数据不受影响。

null和isnull的区别也很简单,前者没有参数,只是作为条件判断,找出为空的记录;而后者是有参数的,它会对第一个参数判断,如果为空就null,不为空就不返回。所以它的执行效率会比is null慢一些。

上面通过一个简单的例子详细的解释了SQL的书写顺序和执行顺序,以加强读者对SQL语句原理的理解,既可以帮助大家搞懂怎么写SQL,也可以为SQL优化提供思路。

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

Guess you like

Origin juejin.im/post/7033939740080635935