暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SQL:谓词、GROUPING 运算符、一维表与二维表转换

小辉数据分析 2021-05-18
558


  • 2.1谓词ANY(SOME)

  • 2.2谓词ALL

  • 2.3谓词EXISTS


一.引言

在SQL中,有三个十分重要的谓词:ANY(SOME)、ALL、EXISTS。三个谓词之间存在什么区别,如何使用,是一个值得我们关注的问题。

GROUP BY是一个我们经常用到的一个SQL子句,其作用相当于Excel中的数据透视表。但是,与Excel中的数据透视表相比,SQL中的GROUP BY子句有个明显的缺陷,那就是无法得到合计结果,同时SQL中的结果是以一维表的形式展示的,这种展示不是很直观。我们如何才能改进GROUP BY的这些缺陷呢?

因此,此文的内容如下:

  • ANY(SOME)、ALL、EXISTS三个谓词的含义与使用技巧
  • GROUPING 运算符(如何实现Excel透视表的汇总功能)
  • PIVOT(如何让结果以二维表的形式呈现)

二.三个谓词的使用方法

2.1谓词ANY(SOME)

ANY和SOME关键字是同义词,表示满足其中任一条件。它们允许创建一个表达式对子查询的返回值列表进行比较,只要满足内层子查询的任一一个比较条件,就返回一个结果作为外层查询的条件。

简单一点来说,这一个谓词的作用和聚合函数min一致!

举个例子说明一下。

-- 创建两张表
create table tb1(num1 int not null);
create table tb2(num2 int not null);
-- 插入数据
insert into tb1 values(1),(5),(13),(27);
insert into tb2 values(6),(14),(11),(20);
-- 返回tb2表的所有num2列,然后将tb1中的num1的值与之比较,只要num1大于num2的值就为符合查询条件的结果
-- 实现方法1
select num1 from tb1 where num1>any(select num2 from tb2);
-- 实现方法2
select num1 from tb1 where num1>(select min(num2) from tb2);

image-20210511162820302

2.2谓词ALL

ALL关键字与ANY和SOME不同,使用ALL时需要同时满足所有内层查询的条件。

以前面的数据集为例,我们举个例子来说明一下。

-- 返回tb1表中比tb2表中num2列所有值都大的值
-- 方法1
select num1 from tb1 where num1>all(select num2 from tb2);
-- 方法2
select num1 from tb1 where num1>(select max(num2) from tb2);

image-20210511164026279

简单总结一下:

  • 谓词any相当于min
  • 谓词all相当于max

2.3谓词EXISTS

EXISTS关键字后面的参数是一个任意的子查询,系统对子查询进行运算以判断它是否返回行,如果至少返回一行,那么EXISTS的结果为True,此时外层查询语句将进行查询;如果子查询没有返回任何行,那么EXISTS返回的结果是FALSE,此时外层语句将不再进行查询。

这么说,显得比较抽象,我们举例说明一下。

我们查询所用的表是我们在SQL练习题中,所用的那四张表:学生表,课程表,教师表与成绩表。

需求1:查找是否有姓李的学生,如果存在姓李的学生,显示其相关信息

select * from Student as t1 
where exists (select * from Student as t2 where t2.sname like '李%' and t1.sname like '李%');

结果如下:

image-20210512154829604

注意,这代代码不能写成如下的形式:

select * from Student as t1 
where exists (select * from Student as t2 where sname like '李%');

这样写,虽然不会报错,但是返回的结果是错误的!

也就是说在PostgreSQL中使用EXISTS语句,必须要使用关联子查询的方式来实现。

现在,我们来讲第二个需求:

需求2:查找姓黄的学生,如果存在则返回相应的生信息。

select * from Student s1
where exists
(select 1 from Student s2 where s1.sname like '黄%' and s2.sname like '黄%');

结果如下:

image-20210512155642967

在本人公众号所发布的文章,文章《SQL练习题(9)》中的第七道题,就可以使用EXISTS语句来进行解答,其具体的写法是:

-- 查询不同课程成绩相同的学生的学生编号、课程编号、学生成绩
select * from SC t1
where exists(select * from SC t2 where t1.cid!=t2.cid and t1.score=t2.score and t1.sid=t2.sid);

结果如下:

image-20210513100000592

三.GROUPING运算符

Excel中拥有一个极其强大的功能:数据透视表。数据透视表的效果如下:

image-20210512163410759

Excel的数据透视表与SQL相比,有一个最大的特点,那就是有一个汇总功能。这意味着我们不仅能查看每一个组成部分的情况,而且意味着我们查看总体的情况。SQL中默认的group by是没有整体汇总的功能的。那么,SQL中如何才能实现这一功能呢?

一种方法是使用UNION ALL子句,将两次查询的结果合并起来,具体如下:

-- 创建数据集
create table items(product_name varchar(10not null,
product_type varchar(10not null,
product_price int  not null);
-- 插入数据
insert into items values('computer','ele_prod',7000);
insert into items values('earphone','ele_prod',500);
insert into items values('pants','clothing',400);
insert into items values('high-heels','clothing',300);
insert into items values('coat','clothing',100);
insert into items values('paper','office',200);
-- 实现
select 'all' as product_type,sum(product_price) as sum_price
from items
union all
select product_type,sum(product_price) as sum_price
from items
group by product_type
order by sum_price desc;

结果如下:

image-20210512165857423

但是,这种操作方式是比较麻烦的。那么,我们有没有比较简单的方式呢?

在MySQL当中,这个需求可以使用rollup来实现,具体代码如下:

select product_type,sum(product_price) as sum_price
from items
group by product_type with rollup;

在其他的SQL数据库软件中,这一功能的实现方式则是:

select product_type,sum(product_price) as sum_price
from items
group by rollup(product_type);

效果如下:

image-20210512171132309

其实,ROLLUP命令是标准SQL中GROUPING运算符中的一种。标准SQL总共支持三种GROUPING运算符,分别是:

  • ROLLUP
  • CUBE
  • GROUPING SETS

为了更好的讲解,我们建立一张新的表。

create table products(
product_id varchar(4) primary key,
product_name varchar(10not null,
product_type varchar(10not null,
sale_price int not null,
purchase_price int,
redis_date date);
insert into products values('0001','t-shirt','garment',1000,500,'2009-09-20');
insert into products values('0002','punch','office',500,320,'2009-09-11');
insert into products values('0003','sport-t','garment',4000,2800,null);
insert into products values('0004','knife','kitchen',3000,2800,'2009-09-20');
insert into products values('0005','pot','kitchen',6800,5000,'2009-01-15');
insert into products values('0006','fork','kitchen',500,null,'2009-09-20');
insert into products values('0007','board','kitchen',880,790,'2008-04-28');
insert into products values('0008','pen','office',100,null,'2009-11-11');

我们先来使用一下rollup语句:

-- 一个字段的名称有错误,我们先更改一下
alter table products rename  redis_date to regis_date;
-- 按日期(年份)和种类进行汇总
select extract(year from regis_date) as year,product_type,sum(sale_price) as sum_price
from products
group by rollup(extract(year from regis_date),product_type);

image-20210513103556364

从结果中,我们可以发现如下信息:

  • 2008年的销售额为880,只销售出厨房用具一个种类的产品,其价值为880元
  • 2009年总共销售出11900元的商品。其中,
    • 服装的销售额是1000
    • 厨房用具的销售额为10300
    • 办公用品的销售额为600
  • 还有4000元的服装销售额,其销售年份是不确定的
  • 总共的销售额是16780

ROLLUP语句,确实功能十分强大,但还是有一些不足的:例如查询结果中有两个4000,但这两个4000的含义是不同的。一个是说有4000元的服装销售额,其销售日期是不确定的;另外一个4000,则是汇总的结果。

为了解决这一个缺陷,我们可以使用GROUPING来解决。

select grouping(extract(year from regis_date)) as year,
grouping(product_type) as product_type,
sum(sale_price) as sum_price
from products
group by rollup(extract(year from regis_date),product_type);

image-20210513110015598

展示出来的结果,可能有点奇怪,我们来解释一下:

  • 0表示数据表中实际的结果。例如第一个880表示某一年的某一类产品的销售额是880元。
  • 1表示汇总的结果或者是原先的数据为null的结果。例如,第二个880表示某一年总的销售额是880元。

GROUPING的显示结果不够直观,不易理解,为了解决这个问题,我们可以使用CASE WHEN 语句。

select case when
grouping(extract(year from regis_date))=1 then 'year_total'
else cast(extract(year from regis_date) as varchar(10)) end as regis_year,
case when
grouping(product_type)=1 then 'product_type total'
else product_type end as product_type,
sum(sale_price) as sum_price
from products
group by rollup(extract(year from regis_date),product_type);

image-20210513112652843

我们还可以进一步地改造:

select coalesce(case when grouping(extract(year from regis_date))=1 then 'year_total'
else cast(extract(year from regis_date) as varchar(10)) end,'not confirmed'as regis_year,
coalesce(case when grouping(product_type)=1 then 'product_type total'
else product_type end,'not confirmed'as product_type,
coalesce(sum(sale_price),0as sum_price
from products
group by rollup(extract(year from regis_date),product_type);

image-20210513113851889

我们对代码做一个简短的说明:

  • coalesce函数用于将null转换成你想指定的数值。
  • cast用于数据类型的转换。case when 的输出结果,其类型要保持一致。

接下来,我们接触一个新的函数cube。废话不多说,直接上例题:

select coalesce(case when
      grouping(regis_date)=1 then 'regis_date total' else cast(regis_date as varchar(30)) end,'not confirmed'as regis_date,
  coalesce(
   case when grouping(product_type)=1 then 'product_type total' else product_type end,'not confirmed'
   as product_type,
  sum(sale_price) as sum_price
from products
group by cube(regis_date,product_type);

image-20210514093452079

我们如何理解这cube呢?

我们再将cube换成rollup,观看一下效果:

select coalesce(case when
      grouping(regis_date)=1 then 'regis_date total' else cast(regis_date as varchar(30)) end,'not confirmed'as regis_date,
  coalesce(
   case when grouping(product_type)=1 then 'product_type total' else product_type end,'not confirmed'
   as product_type,
  sum(sale_price) as sum_price
from products
group by rollup(regis_date,product_type);

image-20210514093819070

我们可以发现cube的结果相比rollup多了三行:

  • 第15行,不限时间,服装类产品的销售额是5000
  • 第16行,不限时间,厨房用品的总销售额是11800
  • 第17行,不限时间,办公用品的总销售额是600

我们现在来总结一下,两个命令在汇总结果上的不同:

  • rollup:
    • group by():不限时间和种类,总的产品销售额(16800)
    • group by(regis_date):以时间为聚合键,不限商品种类的产品销售额,总共返回6条记录(记录序号是2,4,6,9,11,13)
    • group by(regis_date,product_type):同时以时间和种类为聚合键,返回的商品销售额,总共返回7条记录(记录序号是1,3,5,7,8,10,12)
  • cube:
    • 在rollup的基础上,多了一个group by(product_type),即以商品种类为聚合键,不限时间,返回商品的销售额(对应的记录序号是15,16,17)

我们可以看出:cube返回的结果要比rollup多。如果聚合键的个数(也就是rollup或cube的参数数目)为2,rollup返回三种聚合结果;而cube则可以返回四种聚合结果。

实际上,如果聚合键的个数是n,那么rollup会返回(n+1)种聚合结果;cube会返回2的n次方种聚合结果。

我们可以看到,rollup的结果是包含在cube的结果当中的。但是,我们有的时候,并不想得到cube或rollup中的全部结果,我们只想得到一部分结果,此时,我们可以使用grouping sets来完成这一个需求。

举例如下:

select coalesce(case when
      grouping(regis_date)=1 then 'regis_date total' else cast(regis_date as varchar(30)) end,'not confirmed'as regis_date,
  coalesce(
   case when grouping(product_type)=1 then 'product_type total' else product_type end,'not confirmed'
   as product_type,
  sum(sale_price) as sum_price
from products
group by grouping sets(regis_date,product_type);

image-20210514102147875

我们可以看到,只取出了9条结果,分别是:

  • 不区分产品种类,按照时间进行汇总得到的结果
  • 不区分时间,按照产品种类汇总得到的结果

在实际的应用中,GROUPING SETS的应用并不是很多!

现在,我们来稍微总结一下group by,group by rollup,group by  cube,group by grouping sets四个命令在汇总结果上的不同(以regis_date和product_type为例):

  • group by:一种汇总结果
    • group by(regis_date,product_type)
  • group by grouping sets:两种汇总结果
    • group by(regis_date)
    • group by(product_type)
  • group by rollup:三种汇总结果
    • group by()
    • group by (regis_date)
    • group by (regis_date,product_type)
  • group by cube:四种汇总结果
    • group by()
    • group by(regis_date)
    • group by(product_type)
    • group by(regis_date,product_type)

四.PIVOT与UNPIVOT(一维表与二维表之间的转换)

SQL中的group by在功能上,相当于Excel中的数据透视表。不过,两者在结果的展现却存在着不同:SQL是以一维表的形式呈现的,而Excel是以二维表的形式呈现的。在结果的呈现上,二维表更为直观,具有一定的优势,那么,我们如何将一个一维表转换成一个二维表呢?

一维表转换成二维表,我们是可以使用case when 来解决的。

-- 创建视图
create view one_dim as
(select coalesce(cast(regis_date as varchar(30)),'not confirmed'as regis_date,
product_type,
sum(sale_price)
from products
group by regis_date,product_type);
-- 一维表转换为二维表
select regis_date,
sum(case when 
product_type='garment' then sum else null end)as garment,
sum(case when
product_type='office' then sum else null endas office,
sum(case when
product_type='kitchen' then sum else null endas kitchen
from one_dim
group by regis_date;

image-20210514120724863

这里稍微注意一个点:为什么要使用sum这一聚合函数(其实,avg、max、min等其他聚合函数也是可以的):为了后面只使用regis_date这一个聚合键!

这种方法,我们明显可以看到是比较麻烦的,有没有简便一些的方法呢?

MS SQL当中PIVOT命令,可以迅速地解决这一问题!这一次,我们使用了新的数据集!

-- 建立一张表
create table data (id varchar(2),
name varchar(10),
season varchar(10),
num int);
-- 插入数据
insert into data values('01','shoe','spring',15);
insert into data values('01','shoe','summer',25);
insert into data values('01','shoe','fall',35);
insert into data values('02','garment','spring',4);
insert into data values('02','garment','summer',5);
insert into data values('02','garment','fall',7);
-- 使用case when
select name,
avg(case when season='spring' then num else null end) as spring,
avg(case when season='summer' then num else null end) as summer,
avg(case when season='fall' then num else null end) as fall
from data
group by name;
-- 使用PIVOT实现
select name,[spring],[summer],[fall]
from
(select name,season,num from data) p
pivot(sum(num) for season in ([spring],[summer],[fall])) as piv;

结果如下图所示:

image-20210515185134501

好了,一维表如何转换成二维表,我们已经知道了。那么,我们如何将二维表转换成一维表呢?

方法有两种:

  1. UNION ALL+SUM等聚合函数方法
  2. UNPIVOT 方法

我们具体讲解如下,

-- 建立数据集
create table data2(city varchar(10),
      pop2010 int,
      pop2020 int);
insert into data2 values('pek',2187,1878);
insert into data2 values('sha',2487,2177);
insert into data2 values('can',1868,1277);
insert into data2 values('szx',1767,1062);
-- 使用UNION ALL+SUM
select city,'pop2010' as years,
sum(pop2010) as num
from data2
group by city
union all
select city,'pop2020' as years,
sum(pop2020) as num
from data2
group by city;
-- 使用UNPIVOT方法
select city,years,num
from
(select city,pop2010,pop2020 from data2) t
unpivot
(num for years in (pop2010,pop2020))p;

结果如下:

image-20210515211221169

五.内容总结

本文的内容总结如下:

参考文献

  1. 《PostgreSQL11从入门到精通》 李小威著   清华大学出版社
  2. 《SQL基础教程(第二版)》MICK著   人民邮电出版社
  3. 公众号数据分析大本营推文《【SQL常用代码】--SQL实现透视功能》
  4. 公众号数据管道推文《面试常考!SQL行转列和列转行》


文章转载自小辉数据分析,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论