MySQL--HAVING子句的力量

本文根据《SQL进阶教程》([日]MICK/著 吴炎昌/译)所写笔记。

寻找缺失的编号

现在有一张表seqtbl,虽然编号那一列叫做连续编号,但实际上编号并不是连续的,缺失4和7.我们现在要做的就是查找是否有缺失值:

CREATE TABLE SeqTbl
(seq  INTEGER PRIMARY KEY,
 name VARCHAR(16) NOT NULL);

INSERT INTO SeqTbl VALUES(1,	'迪克');
INSERT INTO SeqTbl VALUES(2,	'安');
INSERT INTO SeqTbl VALUES(3,	'莱露');
INSERT INTO SeqTbl VALUES(5,	'卡');
INSERT INTO SeqTbl VALUES(6,	'玛丽');
INSERT INTO SeqTbl VALUES(8,	'本');

mysql> select * from seqtbl;
+-----+--------+
| seq | name   |
+-----+--------+
|   1 | 迪克   |
|   2 ||
|   3 | 莱露   |
|   5 ||
|   6 | 玛丽   |
|   8 ||
+-----+--------+

-- 查找是否有缺失值
mysql> select '存在缺失值的编号' as gap
    -> from seqtbl
    -> having count(*)<>max(seq);
+--------------------------+
| gap                      |
+--------------------------+
| 存在缺失值的编号         |
+--------------------------+

如果这个查询结果只有一行,说明存在缺失的编号,如果一行都没有,说明不存在缺失的编号。大家会注意到,上面的语句没有group by子句,此时整张表会被聚合为一行。这种情况下HAVING子句也是可以使用的。HAVING子句是可以单独使用的。不过这种情况下,就不能在select子句里引用原来的表里的列了,要么就得像实例里一样使用常量,要么就像select count(*)这样使用聚合函数

现在,我们已经知道表中存在缺失值了,那么如何求出缺失的编号是多少呢?

-- 查询缺失编号的最小值
mysql> select min(seq+1) as gap from seqtbl
    -> where (seq+1) not in (select seq from seqtbl);
+------+
| gap  |
+------+
|    4 |
+------+

-- 查询全部缺失值
mysql> select seq+1 as gap from seqtbl
    -> where (seq+1) not in (select seq from seqtbl);
+-----+
| gap |
+-----+
|   4 |
|   7 |
|   9 |
+-----+

用HAVING子句进行子查询:求众数

下面有一张graduates 表,我们求一下众数(方法一,使用谓词。方法二,使用极值函数):

CREATE TABLE Graduates
(name   VARCHAR(16) PRIMARY KEY,
 income INTEGER NOT NULL);

INSERT INTO Graduates VALUES('桑普森', 400000);
INSERT INTO Graduates VALUES('迈克',     30000);
INSERT INTO Graduates VALUES('怀特',   20000);
INSERT INTO Graduates VALUES('阿诺德', 20000);
INSERT INTO Graduates VALUES('史密斯',     20000);
INSERT INTO Graduates VALUES('劳伦斯',   15000);
INSERT INTO Graduates VALUES('哈德逊',   15000);
INSERT INTO Graduates VALUES('肯特',     10000);
INSERT INTO Graduates VALUES('贝克',   10000);
INSERT INTO Graduates VALUES('斯科特',   10000);

mysql> select * from graduates;
+-----------+--------+
| name      | income |
+-----------+--------+
| 劳伦斯    |  15000 |
| 史密斯    |  20000 |
| 哈德逊    |  15000 |
| 怀特      |  20000 |
| 斯科特    |  10000 |
| 桑普森    | 400000 |
| 肯特      |  10000 |
| 贝克      |  10000 |
| 迈克      |  30000 |
| 阿诺德    |  20000 |
+-----------+--------+

-- 求众数
-- 方法一:使用谓词
mysql> select income,count(*) as cnt
    -> from graduates
    -> group by income
    -> having count(*) >=all(select count(*) from graduates group by income);
+--------+-----+
| income | cnt |
+--------+-----+
|  10000 |   3 |
|  20000 |   3 |
+--------+-----+

--方法二:使用极值函数
mysql> select income,count(*) as cnt
    -> from graduates
    -> group by income
    -> having count(*) >= (select max(cnt) from (select count(*) as cnt from graduates group by income)tmp);
+--------+-----+
| income | cnt |
+--------+-----+
|  10000 |   3 |
|  20000 |   3 |
+--------+-----+

用HAVING子句进行自连接:求中位数(没看懂)

如果集合中的元素个数为偶数,则取中间两个元素的平均值作为中位数。做法是,将集合里的元素按照大小分为上半部分和下半部分两个子集,同时让这2个子集共同拥有集合正中间的元素。

mysql> SELECT AVG(DISTINCT income)
    ->   FROM (SELECT T1.income
    ->           FROM Graduates T1, Graduates T2
    ->       GROUP BY T1.income
    ->                /* S1的条件 */
    ->         HAVING SUM(CASE WHEN T2.income >= T1.income THEN 1 ELSE 0 END)
    ->                    >= COUNT(*) / 2
    ->                /* S2的条件 */
    ->            AND SUM(CASE WHEN T2.income <= T1.income THEN 1 ELSE 0 END)
    ->                    >= COUNT(*) / 2 ) TMP;
+----------------------+
| AVG(DISTINCT income) |
+----------------------+
|           17500.0000 |
+----------------------+

查询不包含null的集合

count函数的使用方法有count(*)和count(列名)两种,他们的区别有两个:第一个是性能上的区别,第二个是count(*)可以用于null,而count(列名)与其他聚合函数一样,要先排除掉null的行再进行统计。
下面我们用一张nulltbl表来看一下区别:

mysql> create table nulltbl
    -> (col_1 char(4));
mysql> insert into nulltbl values(null);
Query OK, 1 row affected (0.04 sec)

mysql> insert into nulltbl values(null);
Query OK, 1 row affected (0.02 sec)

mysql> insert into nulltbl values(null);
Query OK, 1 row affected (0.04 sec)

mysql> select * from nulltbl;
+-------+
| col_1 |
+-------+
| NULL  |
| NULL  |
| NULL  |
+-------+

mysql> select count(*),count(col_1) from nulltbl;
+----------+--------------+
| count(*) | count(col_1) |
+----------+--------------+
|        3 |            0 |
+----------+--------------+

例如:这里有一张存储了学生调教报告的日期的表students,学生提交报告后,“提交日期”会被写入到日期,而提交之前是null。现在我们需要从这张表里找出哪些学院的学生全部都提交了报告(即理学院、经济学院)。如果只是用where sbmt_date is not null这样的条件查询,文学院也会被包含进来。正确的做法是以“学院”为group by 的列生成子集。

CREATE TABLE Students
(student_id   INTEGER PRIMARY KEY,
 dpt          VARCHAR(16) NOT NULL,
 sbmt_date    DATE);

INSERT INTO Students VALUES(100,  '理学院',   '2005-10-10');
INSERT INTO Students VALUES(101,  '理学院',   '2005-09-22');
INSERT INTO Students VALUES(102,  '文学院',   NULL);
INSERT INTO Students VALUES(103,  '文学院',   '2005-09-10');
INSERT INTO Students VALUES(200,  '文学院',   '2005-09-22');
INSERT INTO Students VALUES(201,  '工学院',   NULL);
INSERT INTO Students VALUES(202,  '经济学院', '2005-09-25');

mysql> select * from students;
+------------+--------------+------------+
| student_id | dpt          | sbmt_date  |
+------------+--------------+------------+
|        100 | 理学院       | 2005-10-10 |
|        101 | 理学院       | 2005-09-22 |
|        102 | 文学院       | NULL       |
|        103 | 文学院       | 2005-09-10 |
|        200 | 文学院       | 2005-09-22 |
|        201 | 工学院       | NULL       |
|        202 | 经济学院     | 2005-09-25 |
+------------+--------------+------------+

-- 查询“提交日期”列内不包含null的学院:使用count函数
mysql> select dpt from students
    -> group by dpt
    -> having count(*)=count(sbmt_date);
+--------------+
| dpt          |
+--------------+
| 理学院       |
| 经济学院     |
+--------------+

-- 查询“提交日期”列内不包含null的学院:使用case表达式
mysql> select dpt from students
    -> group by dpt
    -> having count(*)=sum(case when sbmt_date is not null
    ->                          then 1 else 0 end);
+--------------+
| dpt          |
+--------------+
| 理学院       |
| 经济学院     |
+--------------+

case表达式将“提交日期”不是null的行标记为1,将“提交日期”为null的行标记为0.

用关系除法运算进行购物篮分析

有两张表,全国连锁折扣店的商品表items,以及各个店铺的库存管理表shopitems。

CREATE TABLE Items
(item VARCHAR(16) PRIMARY KEY);
 
CREATE TABLE ShopItems
(shop VARCHAR(16),
 item VARCHAR(16),
    PRIMARY KEY(shop, item));

INSERT INTO Items VALUES('啤酒');
INSERT INTO Items VALUES('纸尿裤');
INSERT INTO Items VALUES('自行车');

INSERT INTO ShopItems VALUES('仙台',  '啤酒');
INSERT INTO ShopItems VALUES('仙台',  '纸尿裤');
INSERT INTO ShopItems VALUES('仙台',  '自行车');
INSERT INTO ShopItems VALUES('仙台',  '窗帘');
INSERT INTO ShopItems VALUES('东京',  '啤酒');
INSERT INTO ShopItems VALUES('东京',  '纸尿裤');
INSERT INTO ShopItems VALUES('东京',  '自行车');
INSERT INTO ShopItems VALUES('大阪',  '电视');
INSERT INTO ShopItems VALUES('大阪',  '纸尿裤');
INSERT INTO ShopItems VALUES('大阪',  '自行车');

mysql> select * from items;
+-----------+
| item      |
+-----------+
| 啤酒      |
| 纸尿裤    |
| 自行车    |
+-----------+
mysql> select * from shopitems;
+--------+-----------+
| shop   | item      |
+--------+-----------+
| 东京   | 啤酒      |
| 东京   | 纸尿裤    |
| 东京   | 自行车    |
| 仙台   | 啤酒      |
| 仙台   | 窗帘      |
| 仙台   | 纸尿裤    |
| 仙台   | 自行车    |
| 大阪   | 电视      |
| 大阪   | 纸尿裤    |
| 大阪   | 自行车    |
+--------+-----------+

我们要查询的是囊括了表items中所有商品的店铺(仙台和东京)。大阪店没有啤酒,所以不是我们的目标。用一下语句查询结果是错误的:

mysql> select distinct shop
    -> from shopitems
    -> where item in (select item from items);
+--------+
| shop   |
+--------+
| 东京   |
| 仙台   |
| 大阪   |
+--------+

这是因为,在where子句里指定的条件只对表里的某一行数据有效。谓词in的条件其只是指定了“店内有啤酒或者纸尿裤或者自行车的店铺”,所以店铺只要有这三种商品中的任何一种就会出现在查询结果中。
正确的SQL语句应该这样写:

mysql> select si.shop
    -> from shopitems si,items i
    -> where si.item=i.item
    -> group by si.shop
    -> having count(si.item)=(select count(item) from items);
+--------+
| shop   |
+--------+
| 东京   |
| 仙台   |
+--------+

having子句的子查询select count(item) from items的返回结果是常量3.因此,对商品表和店铺的库存管理表进行连接操作后结果是3行的店铺会被选中。而仙台店则因(仙台,窗帘)的行在表连接时会被排除掉,所以也会被选中。东京店连接后的结果时3也会被选中。
但是需要注意的是,如果把having子句改写成having count(si.item)=count(i.item),结果就不对了。仙台、东京、大阪3个店都会被选中。这是因为受到操作的影响,count(i.item)的值和表items原本的行数不一样了。

-- count(i.item)的值已经不一定是3了
mysql> select si.shop,count(si.item),count(i.item)
    -> from shopitems si,items i
    -> where si.item=i.item
    -> group by si.shop;
+--------+----------------+---------------+
| shop   | count(si.item) | count(i.item) |
+--------+----------------+---------------+
| 东京   |              3 |             3 |
| 仙台   |              3 |             3 |
| 大阪   |              2 |             2 |
+--------+----------------+---------------+

接下来,我们看看如何排除掉仙台店(仙台店的仓库存在“窗帘”,但商品表里没有窗帘),让结果只出现京东店。这类问题被称为“精确关系除法”,即只选择没有剩余商品的店铺(与此相对,前一个问题被称为“带余除法”。解决这一问题,我们使用外联结:

mysql> select si.shop
    -> from shopitems si left outer join items i
    -> on si.item=i.item
    -> group by si.shop
    -> having count(si.item)=(select count(item) from items)   -- 条件1
    ->    and count(i.item)=(select count(item) from items);   -- 条件2
+--------+
| shop   |
+--------+
| 东京   |
+--------+

以表shopitems为主表进行外连接后,结果如下:

mysql> select si.shop,si.item,i.item
    -> from shopitems si left outer join items i
    -> on si.item=i.item;
+--------+-----------+-----------+
| shop   | item      | item      |
+--------+-----------+-----------+
| 东京   | 啤酒      | 啤酒      |
| 东京   | 纸尿裤    | 纸尿裤    |
| 东京   | 自行车    | 自行车    |
| 仙台   | 啤酒      | 啤酒      |
| 仙台   | 窗帘      | NULL      |
| 仙台   | 纸尿裤    | 纸尿裤    |
| 仙台   | 自行车    | 自行车    |
| 大阪   | 电视      | NULL      |
| 大阪   | 纸尿裤    | 纸尿裤    |
| 大阪   | 自行车    | 自行车    |
+--------+-----------+-----------+

执行下面语句:

mysql> select si.shop
    -> ,count(si.item),count(i.item)
    -> from shopitems si left outer join items i
    -> on si.item=i.item
    ->  group by si.shop;
+--------+----------------+---------------+
| shop   | count(si.item) | count(i.item) |
+--------+----------------+---------------+
| 东京   |              3 |             3 |
| 仙台   |              4 |             3 |
| 大阪   |              3 |             2 |
+--------+----------------+---------------+

显然,只有东京符合。

本节小结

  1. 表不是文件,记录也没有顺序,所以SQL不进行排序
  2. SQL不是面向过程语言,没有循环、条件分支、赋值操作
  3. SQL通过不断生成子集来求得目标集合。SQL不像面向过程语言那用通过画流程图来思考问题。而是通过画集合的关系图来思考。
  4. group by子句可以用来生成子集。
  5. where子句用来调查集合元素的性指,而having子句用来调查集合本身的性质。

习题1:
在“寻找缺失编号”部分,将SQL语句修改成始终返回一行结果,即存在缺失编号时返回“存在缺失编号”,不存在缺失编号时返回“不存在缺失编号”。

-- 法一:使用union
mysql> select '存在缺失编号' as gap
    -> from seqtbl
    -> having count(*) <> max(seq)
    -> union all
    -> select '不存在缺失编号' as gap
    -> from seqtbl
    -> having count(*) =max(seq);
+--------------------+
| gap                |
+--------------------+
| 存在缺失编号       |
+--------------------+

-- 法二:使用case
mysql> select case when count(*)<>max(seq)
    ->             then '存在缺失编号'
    ->             else '不存在缺失编号' end as gap
    -> from seqtbl;
+--------------------+
| gap                |
+--------------------+
| 存在缺失编号       |
+--------------------+
-- 使用两条case
mysql> select case when count(*)<>max(seq)
    ->             then '存在缺失编号'
    ->             when count(*)=max(seq)
    ->             then '不存在缺失编号'
    ->             else 0 end as gap
    -> from seqtbl;
+--------------------+
| gap                |
+--------------------+
| 存在缺失编号       |
+--------------------+

习题2:
使用students表,查询“全体学生都在9月份提交了报告的学院”,即满足条件的只有经济学院。
首先我使用的是下面的代码,执行结果是错误的:

mysql> select dpt
    -> from students
    -> where sbmt_date between '2005-09-01' and '2005-09-30'
    -> group by dpt
    -> having count(*)=count(sbmt_date);
+--------------+
| dpt          |
+--------------+
| 文学院       |
| 理学院       |
| 经济学院     |
+--------------+

使用case表达式,把在9月份提交完成的学生记为1,否则为0.如果这个case表达式的合计值与集合全体元素的数目一致,就说明该学院的所有学生都在9月份提交完了。所以将代码改写为如下:

mysql> select dpt
    -> from students
    -> group by dpt
    -> having count(*) =sum(case when sbmt_date between '2005-09-01' and '2005-09-30'
    ->                           then 1 else 0 end);
+--------------+
| dpt          |
+--------------+
| 经济学院     |
+--------------+

习题3:
在“用关系除法运算进行购物篮分析”部分,返回结果只选择了满足条件的店铺。下面我们希望对于没有备齐全部商品类型的店铺,我们希望返回的一览表能展示这些店铺缺失多少种商品。my_item_cnt是店铺的现有库存商品种类,diff_cnt是不足商品的商品种类数

mysql> select si.shop,count(si.item) as my_item_cnt,
    ->        (select count(item) from items)-count(si.item) as diff_cnt
    -> from shopitems si,items i
    -> where si.item=i.item
    -> group by si.shop;
+--------+-------------+----------+
| shop   | my_item_cnt | diff_cnt |
+--------+-------------+----------+
| 东京   |           3 |        0 |
| 仙台   |           3 |        0 |
| 大阪   |           2 |        1 |
+--------+-------------+----------+
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值