乐趣区

关于java:神奇的-SQL-之-HAVING-一个容易被忽视的主角

送大家以下 java 学习材料,文末有支付形式

初识 HAVING

对于 SQL 中的 HAVING,置信大家都不生疏,它往往与 GROUP BY 配合应用,为聚合操作指定条件

说到指定条件,咱们最先想到的往往是 WHERE 子句,但 WHERE 子句只能指定行的条件,而不能指定组的条件,因而就有了 HAVING 子句,它用来指定组的条件。咱们来看个具体示例就分明了。

咱们有 学生班级表(tbl\_student\_class)以及 数据如下:

DROP TABLE IF EXISTS tbl_student_class;
CREATE TABLE tbl_student_class (id int(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  sno varchar(12) NOT NULL COMMENT '学号',
  cno varchar(5) NOT NULL COMMENT '班级号',
  cname varchar(50) NOT NULL COMMENT '班级名',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='学生班级表';

-- ----------------------------
-- Records of tbl_student_class
-- ----------------------------
INSERT INTO tbl_student_class(sno, cno, cname) VALUES ('20190607001', '0607', '影视 7 班');
INSERT INTO tbl_student_class(sno, cno, cname) VALUES ('20190607002', '0607', '影视 7 班');
INSERT INTO tbl_student_class(sno, cno, cname) VALUES ('20190608003', '0608', '影视 8 班');
INSERT INTO tbl_student_class(sno, cno, cname) VALUES ('20190608004', '0608', '影视 8 班');
INSERT INTO tbl_student_class(sno, cno, cname) VALUES ('20190609005', '0609', '影视 9 班');
INSERT INTO tbl_student_class(sno, cno, cname) VALUES ('20190609006', '0609', '影视 9 班');
INSERT INTO tbl_student_class(sno, cno, cname) VALUES ('20190609007', '0609', '影视 9 班');

咱们要查问 学生人数为 3 的班级,这就须要用到 HAVING 了,置信大家都会写

SELECT cno, COUNT(*) nums FROM tbl_student_class GROUP BY cno HAVING COUNT(*) = 3;

如果咱们不应用 HAVING,会是什么样呢

能够看到,除了数量等于 3 的班级之前,其余的班级也被查出来了

咱们能够简略总结下:WHERE 先过滤出行,而后 GROUP BY 对行进行分组,HAVING 再对组进行过滤,筛选出咱们须要的组

HAVING 子句的形成因素

既然 HAVING 操作的对象是组,那么其应用的因素是有肯定限度的,可能应用的因素有 3 种:常数、聚合函数 和 聚合键,聚合键也就是 GROUP BY 子句中指定的列名

示例中的 HAVING COUNT(*) = 3,COUNT(*) 是聚合函数,3 是常数,都在 3 因素之中;如果有 3 因素之外的条件,会是怎么样呢

SELECT cno, COUNT(*) nums FROM tbl_student_class GROUP BY cno HAVING cname = '影视 9 班';

执行如上 SQL 会失败,并提醒:

[Err] 1054 - Unknown column 'cname' in 'having clause'

在应用 HAVING 子句时,把 GROUP BY 聚合后的后果作为 HAVING 子句的终点,会更容易了解;示例中通过 cno 进行聚合后的后果如下:

聚合后的这个后果并没有 cname 这个列,那么通过这个列来进行条件解决,当然就报错了啦

仔细的小伙伴应该曾经发现,HAVING 子句的形成因素和蕴含 GROUP BY 子句时的 SELECT 子句的形成因素是一样的,都是只能蕴含 常数、聚合函数 和 聚合键

HAVING 的魅力

HAVING 子句是 SQL 里一个十分重要的性能,是了解 SQL 面向汇合这一实质的要害。上面联合具体的案例,来感触下 HAVING 的魅力

是否存在缺失的编号

tbl\_student\_class 表中记录的 id 是间断的(id 的起始值不肯定是 1),咱们去掉其中 3 条

DELETE FROM tbl_student_class WHERE id IN(2,5,6); SELECT * FROM tbl_student_class;

如何判断是否有编号缺失?

数据量少,咱们一眼就能看进去,然而如果数据量上百万行了,用眼就看不出来了吧

不绕圈子了,我就间接写了,置信大家都能看懂(记得和本人想的比照一下)

SELECT '存在缺失的编号' AS gap FROM tbl_student_class HAVING COUNT(*) <> MAX(id) - MIN(id) + 1;

下面的 SQL 语句里没有 GROUP BY 子句,此时整张表会被聚合为一组,这种状况下 HAVING 子句也是能够应用的(HAVING 不是肯定要和 GROUP BY 一起应用)

写的更谨严点,如下(没有 HAVING,不是配角,看一眼就好)

-- 无论如何都有后果返回
SELECT CASE WHEN COUNT(*) = 0 THEN '表为空'
    WHEN COUNT(*) <> MAX(id) - MIN(id) + 1 THEN '存在缺失的编号'
    ELSE '间断' END AS gap FROM tbl_student_class;

那如何找出缺失的编号了,欢送评论区留言

求众数

假如咱们有一张表:tbl\_student\_salary,记录着毕业生首份工作的年薪

DROP TABLE IF EXISTS tbl_student_salary;
CREATE TABLE tbl_student_salary (id int(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  name varchar(5) NOT NULL COMMENT '姓名',
  salary DECIMAL(15,2) NOT NULL COMMENT '年薪, 单位元',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='毕业生年薪标';

insert into tbl_student_salary values (1,'李小龙', 1000000);
insert into tbl_student_salary values (2,'李四', 50000);
insert into tbl_student_salary values (3,'王五', 50000);
insert into tbl_student_salary values (4,'赵六', 50000);
insert into tbl_student_salary values (5,'张三', 70000);
insert into tbl_student_salary values (6,'张一三', 70000);
insert into tbl_student_salary values (7,'张二三', 70000);
insert into tbl_student_salary values (8,'张三三', 60000);
insert into tbl_student_salary values (9,'张三四', 40000);
insert into tbl_student_salary values (10,'张三丰', 30000);

平均工资达到了 149000 元,乍一看如同毕业生大多都能拿到很高的工资。然而这个数字背地却有一些玄机,因为功夫巨匠李小龙在这一届毕业生中,因为他出众的薪资,将大家的均匀薪资拉升了一大截

简略地求平均值有一个毛病,那就是很容易受到离群值(outlier)的影响。这种时候就必须应用更能精确反映出群体趋势的指标——众数(mode)就是其中之一

那么如何用 SQL 语句来求众数了,咱们往下看

-- 应用谓词 ALL 求众数
SELECT salary, COUNT(*) AS cnt
FROM tbl_student_salary
GROUP BY salary
HAVING COUNT(*) >= ALL (SELECT COUNT(*)
    FROM tbl_student_salary
    GROUP BY salary);

后果如下

ALL 谓词用于 NULL 或空集时会呈现问题,咱们能够用极值函数来代替;这里要求的是元素数最多的汇合,因而能够用 MAX 函数

-- 应用极值函数求众数
SELECT salary, COUNT(*) AS cnt
FROM tbl_student_salary
GROUP BY salary
HAVING COUNT(*) >= (SELECT MAX(cnt)
    FROM (SELECT COUNT(*) AS cnt
        FROM tbl_student_salary
        GROUP BY salary
        ) TMP
    ) ;

求中位数

当平均值不可信时,与众数一样常常被用到的另一个指标是中位数(median)。它指的是将汇合中的元素按升序排列后恰好位于正中间的元素。如果汇合的元素个数为偶数,则取两头两个元素的平均值作为中位数

表 tbl\_student\_salary 有 10 条记录,那么 张三三, 60000 和 李四, 50000 的平均值 55000 就是中位数

那么用 SQL,该如何求中位数呢?做法是,将汇合里的元素依照大小分为上半局部和下半局部两个子集,同时让这 2 个子集独特领有汇合正中间的元素。这样,独特局部的元素的平均值就是中位数,思路如下图所示

像这样须要依据大小关系生成子集时,就轮到非等值自连贯出场了

-- 求中位数的 SQL 语句:在 HAVING 子句中应用非等值自连贯
SELECT AVG(DISTINCT salary)
FROM (
    SELECT T1.salary
    FROM tbl_student_salary T1, tbl_student_salary T2
    GROUP BY T1.salary
    -- S1 的条件
    HAVING SUM(CASE WHEN T2.salary >= T1.salary THEN 1 ELSE 0 END) >= COUNT(*) / 2
    -- S2 的条件
    AND SUM(CASE WHEN T2.salary <= T1.salary THEN 1 ELSE 0 END) >= COUNT(*) / 2
) TMP;

这条 SQL 语句的要点在于比拟条件 >= COUNT(*)/2 里的等号,加上等号并不是为了清晰地离开子集 S1 和 S2,而是为了让这 2 个子集领有独特局部

如果去掉等号,将条件改成 > COUNT(*)/2,那么当元素个数为偶数时,S1 和 S2 就没有独特的元素了,也就无奈求出中位数了;加上等号是为了写出通用性更高的 SQL

查问不蕴含 NULL 的汇合

假如咱们有一张学生报告提交记录表:tbl\_student\_submit\_log

DROP TABLE IF EXISTS tbl_student_submit_log;
CREATE TABLE tbl_student_submit_log (id int(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  sno varchar(12) NOT NULL COMMENT '学号',
  dept varchar(50) NOT NULL COMMENT '学院',
  submit_date DATE COMMENT '提交日期',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='学生报告提交记录表';

insert into tbl_student_submit_log values
(1,'20200607001', '理学院', '2020-12-12'),
(2,'20200607002', '理学院', '2020-12-13'),
(3,'20200608001', '文学院', null),
(4,'20200608002', '文学院', '2020-12-22'),
(5,'20200608003', '文学院', '2020-12-22'),
(6,'20200612001', '工学院', null),
(7,'20200617001', '经济学院', '2020-12-23');

学生提交报告后,submit\_date 列会被写入日期,而提交之前是 NULL

当初咱们须要从这张表里找出哪些学院的学生全副都提交了报告,这个 SQL 该怎么写?

如果只是用 WHERE submit\_date IS NOT NULL 条件进行查问,那文学院也会被蕴含进来,后果就不正确了

正确的做法应该先以 dept 进行分组(GROUP BY),而后对组进行条件的过滤,SQL 如下

SELECT dept
FROM tbl_student_submit_log
GROUP BY dept
HAVING COUNT(*) = COUNT(submit_date);

这里其实用到了 COUNT 函数,COUNT(*) 能够用于 NULL,而 COUNT(列名) 与其余聚合函数一样,要先排除掉 NULL 的行再进行统计

当然,应用 CASE 表达式也能够实现同样的性能,而且更加通用

SELECT dept
FROM tbl_student_submit_log
GROUP BY dept
HAVING COUNT(*) = SUM(
    CASE WHEN submit_date IS NOT NULL THEN 1
        ELSE 0 END
    );

其余

不仅仅只是如上的那些场景实用于 HAVING,还有很多其余的场景也是须要用到 HAVING 的,有趣味的能够去翻阅《SQL 进阶教程》

聚合键条件的归属

咱们来看个乏味的货色,还是用表:tbl\_student\_class

image-20210712181154110

咱们发现,聚合键所对应的条件既能够写在 HAVING 子句当中,也能够写在 WHERE 子句当中

尽管条件别离写在 HAVING 子句和 WHERE 子句当中,然而条件的内容,以及返回的后果都完全相同,因而,很多小伙伴就会感觉两种书写形式都没问题

单从后果来看,的确没问题,但其中有一种属于偏离了 SQL 标准的非正规用法,举荐做法是:聚合键所对应的条件应该书写在 WHERE 子句中,理由有二

语义更清晰

WHERE 子句和 HAVING 子句的作用是不同的;后面曾经说过,HAVING 子句是用来指定“组”的条件的,而“行”所对应的条件应该写在 WHERE 子句中,这样一来,写进去的 SQL 语句岂但能够分清两者各自的性能,而且了解起来也更容易

执行速度更快

应用 COUNT 等函数对表中数据进行聚合操作时,DBMS 外部进行排序解决,而排序解决会大大增加机器的累赘,从而升高处理速度;因而,尽可能减少排序的行数,能够进步处理速度

通过 WHERE 子句指定条件时,因为排序之前就对数据进行了过滤,那么就缩小了聚合操作时的须要排序的记录数量;而 HAVING 子句是在排序之后才对数据进行分组的,与在 WHERE 子句中指定条件比起来,须要排序的数量就会多得多

另外,索引是 WHERE 依据速度劣势的另一个无利反对,在 WHERE 子句指定条件所对应的列上创立索引,能够大大提高 WHERE 子句的处理速度

总结

1、集合论

集合论是 SQL 语言的根基,只有从汇合的角度来思考,能力明确 SQL 的弱小威力

学习 HAVING 子句的用法是帮忙咱们顺利地忘掉面向过程语言的思考形式并了解 SQL 面向汇合个性的最为无效的办法

2、HAVING 子句的因素

3 个因素:常数、聚合函数 和 聚合键

HAVING 大多数状况下和联合 GROUP BY 来应用,但不是肯定要联合 GROUP BY 来应用

3、SQL 的执行程序

WHERE 子句是指定行所对应的条件,而 HAVING 子句是指定组所对应的条件

退出移动版