这一部分主要使用SQL中的DML,数据库操作语言(data manipulation language),对数据库数据进行增、删、改、查操作,作为前提,先介绍关系型数据库中的表关系。
MySQL作为关系型数据库,数据是存储在表中的,假如设计一个订单系统,可能包含以下信息:
每个表中都有个特殊的列(字段),称为主键(Primary Key),其值用来唯一标识表中的一行数据(记录)。表中还可以有称为外键(Foreign Key)的列,引用同一个表或不同表中某行的主键。因此,通过外键,表与表之间是有关联关系的,这也是建立关系型数据库模型的基础。
下面我们试着设计一下这个订单系统的表结构,就拿其中的供应商(Vendors)和产品目录(Products)来说:
供应商表(Vendors):
列 | 说明 | 主键/外键 |
---|---|---|
vendor_id | 供应商唯一ID, | PK |
vend_name | 供应商名字 | |
vend_address | 供应商地址 |
产品目录表(Products):
列 | 说明 | 主键/外键 |
---|---|---|
prod_id | 产品唯一ID | PK |
prod_name | 产品名字 | |
prod_price | 产品价格 | |
vendor_id | 供应商ID(关联Vendors表的vendor_id列) | FK |
Products表存放产品记录,每个产品都有prod_name, prod_price,并且有唯一的prod_id作为主键。除此之外,还有一个外键vendor_id,引用Vendors表中的vend_id,通过这种方式,为每个产品指定了供应商。我们不需要在产品表中再添加更多的列来存储其供应商的具体信息,以后直接根据这个外键,去Vendors表中查询即可。
从中我们可以看出,关系型数据库在存储数据时是很高效的,避免了数据冗余。而且,关系型数据库的可伸缩性远比非关系型数据库要好。比如,对数据库中供应商名字,地址等信息的修改,可以只更新Vendors表中的记录,相关表中的记录不用改动。
但是另一方方面,将数据分别存放在不同的表中有时是很复杂的,我们需要查询多个关联的表,才能拿到我们想要的数据。因此,在设计表结构,以及编写SQL语句时,需要很好地理这种关联关系,知道什么信息存在什么表中。
比如上面的供应商和产品,一个供应商可能提供多个产品,而一个产品只能属于一个供应商,在多的一边(产品表)通过Foreign Key 描述这种关系。
比如,一张订单对应一张订单详情;一篇文章摘要,对应一篇具体的文章内容。一对一关系的Foreign Key建在关系的哪边都行。
这个是相对复杂的一种,但是也很好理解。还是上面的例子,比如给产品打标签,一个产品可能有多个标签(”进口“, ”新品“),而一个标签下可以对应多个产品(进口商品可能有很多)。
特别要注意的是,如果要描述多对多关系,需要借助第三张表,这个表称为关系表,这张表实际存储时,看起来是这样的:
id (PK) | prod_id (FK) | tag_id (FK) |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 1 |
4 | 3 | 1 |
除了自增的主键id,它还有两个外键,一个prod_id关联产品表,一个tag_id关联标签表。
这里将以教学管理为例,来创建数据库,创建表,插入数据。最后一部分的综合练习,将基于接下来要创建的数据。
首先,创建一个数据库,就命名为school
吧
CREATE DATABASE school CHARSET utf8;
关于表创建和约束的更多详细,请参考MySQL基础(二)数据库、表的创建及操作
下面我们我们将创建5张表,分别是:
其中班级表和课程表是一对多关系;班级表和学生表是一对多关系;教师表和课程表是一对多关系;学生表和课程表是多对多关系。
-- -------------------------
-- 创建班级表
-- -------------------------
CREATE TABLE Class ( cls_id int NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK caption char(30) NOT NULL );
-- -------------------------
-- 创建课程表
-- -------------------------
CREATE TABLE Course ( crs_id int NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK crs_name char(30) NOT NULL , teacher_id int NOT NULL -- FK );
-- -------------------------
-- 创建成绩表
-- -------------------------
CREATE TABLE Score ( scr_id int NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK num tinyint NOT NULL , student_id int NOT NULL , -- FK course_id int NOT NULL -- FK );
-- -------------------------
-- 创建学生表
-- -------------------------
CREATE TABLE Student ( std_id int NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK gender enum('女', '男') NOT NULL , std_name char(30) NOT NULL , class_id int NOT NULL -- FK );
-- -------------------------
-- 创建教师表
-- -------------------------
CREATE TABLE Teacher ( tea_id int NOT NULL PRIMARY KEY AUTO_INCREMENT , -- PK tea_name char(30) NOT NULL );
-- -------------------------
-- 定义外键
-- -------------------------
ALTER TABLE Course ADD CONSTRAINT FK_Course_Teacher FOREIGN KEY (teacher_id) REFERENCES Teacher (tea_id);
ALTER TABLE Score ADD CONSTRAINT FK_Score_Student FOREIGN KEY (student_id) REFERENCES Student (std_id);
ALTER TABLE Score ADD CONSTRAINT FK_Score_Course FOREIGN KEY (course_id) REFERENCES Course (crs_id);
ALTER TABLE Student ADD CONSTRAINT FK_Student_Class FOREIGN KEY (class_id) REFERENCES Class (cls_id);
注意:定义外键的数据类型一定要和关联表主键的数据类型一致!
CREATE TABLE Course ( crs_id int PRIMARY KEY AUTO_INCREMENT , crs_name char(30) NOT NULL , teacher_id int NOT NULL , CONSTRAINT FK_Course_Teacher -- 外键名 FOREIGN KEY (teacher_id) -- 外键字段 REFERENCES Teacher (tea_id) -- 关联表主键字段 );
CREATE TABLE Course ( crs_id int PRIMARY KEY AUTO_INCREMENT , crs_name char(30) NOT NULL , teacher_id int NOT NULL );
ALTER TABLE Course ADD CONSTRAINT FK_Course_Teacher -- 外键名 Foreign KEY (teacher_id) REFERENCES Teacher (tea_id); -- 外键字段和关联表主键字段
格式
ALTER TABLE 表名 DROP FOREIGN KEY 外键名;
示例
ALTER TABLE Course DROP FOREIGN KEY FK_Course_Teacher;
格式
INSERT INTO 表名 [(fld1, fld2, ..., fldn)]
VALUES (val1, val2, ..., valn);
注意:表名后的字段名可以省略,但建议加上,VALUES
的值将依次匹配字段名,而不一定非要按照表中字段的实际次序;有默认值的字段或自增字段可不提供值。另外,对于自增id,如果插入数据时指定了非连续的值,比如表记录最后一行id是3,插入数据时指定id是9,那么以后再插入数据时,id将从9开始自增。
格式
INSERT INTO 表名 [(fld1, fld2, ..., fldn)]
VALUES (val1, val2, ..., valn),
(val1, val2, ..., valn),
-- ...
(val1, val2, ..., valn);
下面,我们将为上面创建的那5张表填充数据:
-- -------------------------- -- 插入班级数据 -- -------------------------- INSERT INTO Class (caption) VALUES ('三年二班'), ('三年三班'), ('一年二班'), ('二年九班'); -- -------------------------- -- 插入教师数据 -- -------------------------- INSERT INTO Teacher (tea_name) VALUES ('张磊老师'), ('李平老师'), ('刘海燕老师'), ('朱云海老师'), ('李杰老师'); -- ------------------------- -- 插入课程数据 -- ------------------------- INSERT INTO Course (crs_name, teacher_id) VALUES ('生物', 1), ('物理', 2), ('体育', 3), ('美术', 2); -- ------------------------- -- 插入学生数据 -- ------------------------- INSERT INTO Student (gender, std_name, class_id) VALUES ('男', '理解', 1), ('女', '钢蛋', 1), ('男', '张三', 1), ('男', '张一', 1), ('女', '张二', 1), ('男', '张四', 1), ('女', '铁锤', 2), ('男', '李三', 2), ('男', '李一', 2), ('女', '李二', 2), ('男', '李四', 2), ('女', '如花', 3), ('男', '刘三', 3), ('男', '刘一', 3), ('女', '刘二', 3), ('男', '刘四', 3); -- -------------------------- -- 插入成绩数据 -- -------------------------- INSERT INTO Score (num, student_id, course_id) VALUES (10,1,1),(9,1,2),(66,1,4),(8,2,1),(68,2,3),(99,2,4),(77,3,1),(66,3,2), (87,3,3),(99,3,4),(79,4,1),(11,4,2),(67,4,3),(100,4,4),(79,5,1),(11,5,2), (67,5,3),(100,5,4),(9,6,1),(100,6,2),(67,6,3),(100,6,4),(9,7,1),(100,7,2), (67,7,3),(88,7,4),(9,8,1),(100,8,2),(67,8,3),(88,8,4),(91,9,1),(88,9,2), (67,9,3),(22,9,4),(90,10,1),(77,10,2),(43,10,3),(87,10,4),(90,11,1),(77,11,2), (43,11,3),(87,11,4),(90,12,1),(77,12,2),(43,12,3),(87,12,4),(87,13,3);
格式:
UPDATE 表名
SET fld1=val1, fld2=val2, ... -- 赋值,多个字段之间以逗号分隔
WHERE 条件; -- 过滤条件
注意:如果没有WHERE
子句,将更新所有的行!
示例:
-- 将学号为2,课程id为1 的成绩加10分
UPDATE score SET num = num + 10 WHERE student_id=2 and course_id=1;
DELETE FROM 表名 WHERE 条件; -- 过滤条件
删除记录:先查出所有符合条件的记录,然后一条条删除,数据很多比较慢。如果有自增id,再插入新数据时,id会在原有的基础上自增。
如果需要删除整张表的记录,那么使用TRUNCATE
速度更快,它是直接删除表,然后新建一张带原来字段的表。
TRUNCATE TABLE 表名;
基本格式:
SELECT fld, ... -- 选择字段
FROM tablename -- 选择表
[WHERE ...] -- WHERE 条件
[GROUP BY ...] -- 分组
[HAVING ...] -- HAVING 条件
[ORDER BY ...] -- 排序
LIMIT ... ; -- 限制条数
数据库表一般包含大量的数据,很少需要检索表中的所有行,通常会根据需要指定过滤条件。WHERE
子句就是用来指定过滤条件的。
操作符 | 说明 | 操作符 | 说明 |
---|---|---|---|
= | 等于 | LIKE ‘_…%’ | 模糊搜索 |
!= | 不等于 | _ | 通配符:匹配一个字符 |
> | 大于 | % | 通配符:匹配任意字符任意个数 |
>= |
大于等于 | AND | 逻辑与 |
< | 小于 | NOT | 逻辑非 |
<= | 小于等于 | OR | 逻辑或 |
BETWEEN x AND y | 在x与y之间(左右包含) | REGEXP | 正则匹配 |
IN (val1, val2, …) | 指定条件范围 |
格式:
SELECT fieldname AS fieldname_new FROM tablename;
示例:
SELECT tea_id as ID, tea_name as 教师姓名 FROM Teacher;
+----+------------+
| ID | 教师姓名 | +----+------------+
| 1 | 张磊老师 |
| 2 | 李平老师 |
| 3 | 刘海燕老师 |
| 4 | 朱云海老师 |
| 5 | 李杰老师 | +----+------------+
5 rows in set (0.00 sec)
-- 也可以省略AS
SELECT tea_id ID, tea_name 教师姓名 FROM Teacher;
假如有学生各科成绩表,使用别名表示学生总分,使用+
将列值相加
SELECT name as 姓名, math + chinese + english as 总成绩 FROM ExamResult; -- 表不存在,这里只是示例
根据指定列排序,ASC
升序,DESC
降序
-- 查询成绩表中课程id为2的分数和学生,并以分数倒序显示
SELECT num AS 分数, student_id AS 学生ID FROM Score WHERE course_id = 2
ORDER BY num DESC;
+------+--------+
| 分数 | 学生ID | +------+--------+
| 100 | 6 |
| 100 | 7 |
| 100 | 8 |
| 88 | 9 |
-- ...... +------+--------+
11 rows in set (0.14 sec)
-- 查询学生表中3个姓张的学生
SELECT std_id, gender, std_name
FROM Student
WHERE std_name LIKE '张%' LIMIT 3; +--------+--------+----------+
| std_id | gender | std_name | +--------+--------+----------+
| 3 | 男 | 张三 |
| 4 | 男 | 张一 |
| 5 | 女 | 张二 | +--------+--------+----------+
3 rows in set (0.28 sec)
使用LIMIT
可以提高数据库性能。对于上面的这个模糊搜索,即使已经找到了,也将遍历整个数据库,如果数据库中存储了海量数据,这种全表扫描将极大影响性能。使用LIMIT后,找到指定条数的记录后,就会停止查找。
LIMIT [offset,] rows
其实可以接受两个参数,第一参数是偏移。默认不写是偏移0。在最后综合练习的第20题,你将看到它的应用。
聚合函数用来汇总数据:
函数 | 说明 |
---|---|
AVG() |
返回某列的平均值 |
COUNT() |
返回某列的行数 |
MAX() |
返回某列的最大值 |
MIN() |
返回某列的最小值 |
SUM() |
返回某列子和 |
-- 从成绩表中,查找学生id是3的学生的平均成绩
SELECT AVG(num) AS avg_num FROM Score WHERE student_id=3;
+---------+
| avg_num | +---------+
| 82.2500 | +---------+
该函数有两种使用方式:
COUNT(*)
对表中行的数目进行计数COUNT(column)
对特定列中具有值的行进行计数(会忽略列值为NULL的行)-- 查找学生总数
SELECT COUNT(*) AS total_num FROM Student; +-----------+
| total_num | +-----------+
| 16 | +-----------+
MAX()
一般用来找出最大的数值或日期,对于文本数据,将返回该列排序后的最后一行SELECT
选择多列,对其中一列使用MAX()
统计最大值,比如下面这个例子,查询出来的某列具有最大值的行和其它列所在的行,并不是同一行-- 统计课程3的最高成绩
SELECT student_id, MAX(num) FROM Score WHERE course_id=3;
+------------+----------+
| student_id | MAX(num) | +------------+----------+
| 2 | 87 | +------------+----------+
-- 事实上,课程3最高成绩对应的学生id是3
SELECT student_id, num FROM Score WHERE course_id=3
ORDER BY num DESC
LIMIT 3;
+------------+-----+
| student_id | num | +------------+-----+
| 3 | 87 |
| 13 | 87 |
| 2 | 68 | +------------+-----+
-- 第一次查询结果中的2,只是第一次出现课程3的那一行的student_id SELECT student_id, num
FROM Score
WHERE course_id=3 LIMIT 3; +------------+-----+
| student_id | num | +------------+-----+
| 2 | 68 |
| 3 | 87 |
| 4 | 67 | +------------+-----+
MIN()
函数同理,不赘述。
求最大值和最小值,有时候通过DESC ASC排序,配合LIMIT更方便,比如最后综合练习中的19题。
-- 计算订单号为20005的订单总金额
SELECT SUM(item_price*quantity) AS total_price FROM OrderItems WHERE order_num = 20005;
注意:
如上所示,利用标准的算术操作符,所有的聚集函数都可以用来执行多个列上的计算!
以查询平均成绩为例,二者的结果是不一样的,DISTINCT参数会过滤掉重复的成绩再计算
SELECT AVG(num) FROM Score; -- 默认行为是 ALL
+----------+
| AVG(num) | +----------+
| 67.2979 | +----------+
SELECT AVG(DISTINCT num) FROM Score;
+-------------------+
| AVG(DISTINCT num) | +-------------------+
| 60.2941 | +-------------------+
在最后综合练习的第11题中,你也将看到相关应用。
SELECT语句可以根据需要包含多个聚集函数,比如:
SELECT COUNT(*) as num_items, MIN(num) as score_min, MAX(num) AS score_max, AVG(num) AS score_avg FROM Score; +-----------+-----------+-----------+-----------+
| num_items | score_min | score_max | score_avg | +-----------+-----------+-----------+-----------+
| 47 | 9 | 100 | 67.2979 | +-----------+-----------+-----------+-----------+
函数 | 说明 |
---|---|
UPPER() |
大写 |
LOW() |
小写 |
LENGTH() |
返回字符串长度(Bytes) |
SOUNDEX() |
返回字符串的SOUNDEX值 |
CONCAT() |
拼接字段/字符串 |
LENGTH()
长度是字节(在utf8编码中,一个中文字符占3个字节)
SOUNDEX()
将任何文本字符串转化为描述其语言表示的字母数字模式的算法,SOUNDEX 考虑了类似的发音字符和音节,使得能够对字符串进行发音比较,而不是字母比较。(看这个介绍就知道,不支持中文,实测确实如此,很遗憾…)这里给出一个例子:
假如有一张顾客表,其联系名为Michael Green,但如果记错了,输入了 Michelle Green 来查找,肯定是找不到的。如果用SOUNDEX()
函数进行搜索,就可以解决这个问题,它匹配所有发音类似于Michael Green的联系名
SELECT cust_name, cust_contact FROM Customers WHERE SOUNDEX(cust_contact) = SOUNDEX('Michael Green');
CONCAT()
MySQL使用CONCAT函数来拼接字段和字符串(其它DBMS中可能使用 +
和 ||
)
-- 将tea_id字段与tea_name字段联合显示
SELECT CONCAT('(', tea_id, ')', tea_name) AS '(ID)姓名'
FROM Teacher;
+---------------+
| (ID)姓名 | +---------------+
| (1)张磊老师 |
| (2)李平老师 |
| (3)刘海燕老师 |
| (4)朱云海老师 |
| (5)李杰老师 | +---------------+
这一块儿的函数比较多,要用的时候可以根据需求,去查询文档
GROUP BY
可以将数据分为多个逻辑组,再对每个组进行聚合计算
注意区别WHERE
和HAVING
:
WHERE
过滤行,HAVING
过滤分组WHERE
在数据分组前进行过滤, HAVING
在数据分组后进行过滤,结合GROUP BY
子句使用-- 具有两个以上的产品,且产品价格大于4的供应商
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
WHERE prod_price >= 4 -- 产品价格大于4 GROUP BY vend_id -- 根据供应商分组
HAVING COUNT(*) >= 2 -- 具有两个以上的产品
-- 从成绩表中,查询每门课的平均成绩
SELECT AVG(num) AS 'score_avg', course_id FROM Score GROUP BY course_id;
+-----------+-----------+
| score_avg | course_id | +-----------+-----------+
| 54.2500 | 1 |
| 65.0909 | 2 |
| 64.4167 | 3 |
| 85.2500 | 4 | +-----------+-----------+
4 rows in set (0.00 sec)
这里将建立外键的表称为子表,外键引用的表称为父表(主表)。
外键约束是双重的:
外键约束的四种ON语句方式:
级联删除:如果父表的记录被删除,那么子表中对应的记录也被自动删除
FOREIGN KEY (xx_id) REFERENCES parent_tablename (id)
ON DELETE CASCADE;
删除父表中某条记录时,将子表中引用了该记录的外键设为NULL
FOREIGN KEY (xx_id) REFERENCES parent_tablename (id)
ON DELETE SET NULL;
如果子表中引用了父表记录,则不允许对父表中的该记录进行删除。
在MySQL中同RESTRICT。
通过UNION操作符,可以执行多个SELECT查询,并将结果作为一个查询结果返回,这就是组合查询(也称并查询,复合查询 compound query)。
UNION使用场景:
使用形式:
SELECT column1, column2, ...
FROM tablename
WHERE ...
UNION -- 使用UNION连接多个查询
SELECT column1, column2, ...
FROM tablename
WHERE ...
比如,从学生表中查询性别为“男”,或者 姓”李“的学生:
SELECT std_id, std_name, gender FROM Student WHERE gender = '男'
UNION
SELECT std_id, std_name, gender FROM Student WHERE std_name LIKE '李%'; +--------+----------+--------+
| std_id | std_name | gender | +--------+----------+--------+
| 1 | 理解 | 男 |
| 3 | 张三 | 男 |
| 4 | 张一 | 男 |
| 6 | 张四 | 男 |
| 8 | 李三 | 男 |
| 9 | 李一 | 男 |
| 11 | 李四 | 男 |
| 13 | 刘三 | 男 |
| 14 | 刘一 | 男 |
| 16 | 刘四 | 男 |
| 10 | 李二 | 女 | +--------+----------+--------+
当然以上要求也可以用WHERE子句来做,可能还更简洁,用OR
连接两个条件即可。事实上,UNION和WHERE常常是可以互换的,但是对于较为复杂的过滤条件,或者从多个表中检索数据时,使用UNION可能会更简单。
可以看到UNION非常容易使用,但在进行组合时,要注意几条规则:
默认UNION会从查询结果中取出重复的行,如果不希望这么做,可以使用UNION ALL
在组合查询中,如果要对查询结果排序,只能使用一条ORDER BY子句,且必须位于最后一条SELECT语句之后。
多表查询需要利用联结,使多个表返回一组输出。联结是SQL中最重要,最强大的特性!
最基本的方式是,在查询时,指定要联结的所有表,以及关联它们的方式即可。
基本格式:
SELECT column1, column2, ... -- 选择位于不同表中要检索的列
FROM tablename1, tablename2 -- 要联结的表
WHERE ... -- 指定联结条件,非常重要!!!
我们看一个例子:
SELECT crs_name, tea_name
FROM Course, Teacher
WHERE Course.teacher_id = Teacher.tea_id; -- 完全限定列名
+----------+------------+
| crs_name | tea_name | +----------+------------+
| 生物 | 张磊老师 |
| 物理 | 李平老师 |
| 体育 | 刘海燕老师 |
| 美术 | 李平老师 | +----------+------------+
完全限定列名:用一个句点分隔表名和列名,以避免列名混淆。
如果没有WHERE子句指定联结条件,第一个表中的每一行将与第二个表中的每一行配对,而不管它们逻辑上是否能匹配在一起。这样返回的结果称为笛卡儿积,检索出的行的数目等于两个表行数的乘积。
上面这种联结方式称为等值联结(equijoin),也叫内联接(inner join),它基于两个表之间的相等测试。
我们也可以使用另一种的语法,明确指定联结的类型:
SELECT column1, column2, ... -- 选择位于不同表中要检索的列
FROM tablename1 INNER JOIN tablename2 -- 要联结的表
ON ... -- ON指定联结条件,非常重要!!!
下面的SELECT语句返回与上面例子完全相同的结果:
SELECT crs_name, tea_name FROM Course INNER JOIN Teacher ON Course.teacher_id = Teacher.tea_id;
之前的AS别名也可以用在联结中,好处是:
我们将上面的例子用AS别名改写下:
SELECT crs_name, tea_name FROM Course AS c INNER JOIN Teacher AS t ON c.teacher_id = t.tea_id;
这样做还有一个好处就是,一旦联结的多张表中出现了字段名重复,可以通过别名.字段名
(完全限定列名)的方式加以区分:
SELECT a.name, b.name FROM TableA AS a INNER JOIN TableB AS b ON a.fk = b.pk;
自联结(self-join),联结的两张表是相同的表,通过AS别名来区分。
在最后的综合练习第8题,第21题,你将看到它的应用。
假如有一个父表,和一个子表,子表通过外键引用父表中的记录。但并不是父表中的所有记录都会被引用,比如顾客表(父表)和订单表(子表),肯定会有没有下单的顾客。如果在统计顾客的订单数时,希望将没有下单的顾客也包括进来,那么用内联结就不合适。这个时候就要用到外联结。
在内联结的基础上,增加左边有,右边没有的结果,没有的部分显示为Null。注意:左右指的是联结条件的两段。
使用左外连接来查询所有顾客的订单,包括那些没下单的顾客时,可以这样写
SELECT Customers.cust_id, Orders.order_num FROM Customers LEFT JOIN Orders ON Customers.cust_id = Orders.cust_id; -- 联结条件
在内联结的基础上,增加右边有,左边没有的结果,没有的部分显示为Null。
如果将联结条件顺序调换一下,左右联结的效果是一样的。
SELECT Customers.cust_id, Orders.order_num FROM Customers RIGHT JOIN Orders ON Orders.cust_id = Customers.cust_id; -- 联结条件
在内连接的基础上增加左边有右边没有的,和右边有左边没有的结果。MySQL是不支持的FULL JOIN的!但是可以通过UNIO组合左外联结和右外联结,达到同样的效果。
子查询就是嵌套在其它查询中的查询,注意:MySQL4.1之前的版本是不支持这一特性的。
使用场景:
将一条SELECT语句的返回结果作为另一条SELECT语句的WHERE子句
SELECT cln FROM tablename WHERE cln IN (SELECT cln FROM tablename WHERE cln IN (SELECT cln FROM tablename WHERE 查询条件));
在这里,子查询由内向外处理,对于嵌套子查询的数目也没有限制,但在实际使用时,出于性能考虑,不要嵌套太多的子查询。
假如有一个顾客表Customers和订单表Orders,Orders表通过cust_id外键引用Customers表中的cust_id主键。要求查询Customers表中每个顾客的订单总数。
要完成以上需求,需要以下两步:
下面利用子查询完成这一需求
SELECT cust_name, (SELECT COUNT(*) FROM Orders WHERE Orders.cust_id = Customers.cust_id) AS orders FROM Customers; +---------------+--------+
| cust_name | orders | +---------------+--------+
| Village Toys | 2 |
| Kids Place | 0 |
| Fun4All | 1 |
| Fun4All | 1 |
| The Toy Store | 1 | +---------------+--------+
说明:orders是一个计算字段,它由子查询建立,该子查询对检索出的每个顾客执行一次。这里也使用了完全限定列名,以避免列名混淆。
通过联结,也可以完成这一需求:
SELECT cust_name, COUNT(Orders.order_num) AS orders FROM Customers LEFT JOIN Orders ON Customers.cust_id = Orders.cust_id GROUP BY Customers.cust_id;
要注意的一点是,这里COUNT后不能用*
,否则就是对行作统计,必须指定列名,这样如果该列没有值(顾客没有订单NULL),就不会被统计进去。
这里将以插入数据部分创建的教学管理数据为基础,进行练习。
SELECT crs_name, tea_name FROM Course LEFT JOIN Teacher ON Course.teacher_id = Teacher.tea_id;
+----------+------------+
| crs_name | tea_name | +----------+------------+
| 生物 | 张磊老师 |
| 物理 | 李平老师 |
| 体育 | 刘海燕老师 |
| 美术 | 李平老师 | +----------+------------+
4 rows in set (0.00 sec)
SELECT gender, COUNT(*) AS total FROM Student GROUP BY gender;
+--------+-------+
| gender | total | +--------+-------+
| 女 | 6 |
| 男 | 10 | +--------+-------+
2 rows in set (0.00 sec)
SELECT std_name, num FROM Student INNER JOIN Score ON Score.course_id = (SELECT crs_id FROM Course WHERE crs_name='物理') AND Score.num = 100 AND Score.student_id = Student.std_id;
+----------+-----+
| std_name | num | +----------+-----+
| 张四 | 100 |
| 铁锤 | 100 |
| 李三 | 100 | +----------+-----+
3 rows in set (0.00 sec)
-- 将成绩根据学生id分组
SELECT std_name, AVG(Score.num) FROM Student LEFT JOIN Score ON Student.std_id = Score.student_id GROUP BY Score.student_id HAVING AVG(Score.num)>80;
+----------+----------------+
| std_name | AVG(Score.num) | +----------+----------------+
| 张三 | 82.2500 |
| 刘三 | 87.0000 | +----------+----------------+
2 rows in set (0.04 sec)
SELECT std_id, std_name, COUNT(Score.course_id), SUM(Score.num) FROM Student LEFT JOIN Score ON Student.std_id = Score.student_id GROUP BY Student.std_id;
+--------+----------+------------------------+----------------+
| std_id | std_name | COUNT(Score.course_id) | SUM(Score.num) | +--------+----------+------------------------+----------------+
| 1 | 理解 | 3 | 85 |
| 2 | 钢蛋 | 3 | 185 |
| 3 | 张三 | 4 | 329 |
| 4 | 张一 | 4 | 257 |
| 5 | 张二 | 4 | 257 |
| 6 | 张四 | 4 | 276 |
| 7 | 铁锤 | 4 | 264 |
| 8 | 李三 | 4 | 264 |
| 9 | 李一 | 4 | 268 |
| 10 | 李二 | 4 | 297 |
| 11 | 李四 | 4 | 297 |
| 12 | 如花 | 4 | 297 |
| 13 | 刘三 | 1 | 87 |
| 14 | 刘一 | 0 | NULL |
| 15 | 刘二 | 0 | NULL |
| 16 | 刘四 | 0 | NULL | +--------+----------+------------------------+----------------+
16 rows in set (0.00 sec)
SELECT COUNT(*) AS Li_nums FROM Teacher WHERE tea_name LIKE '李%';
+---------+
| Li_nums | +---------+
| 2 | +---------+
1 row in set (0.00 sec)
-- 找出选了李平课的学生,取反; 联结之后再过滤
SELECT std_name FROM Student WHERE std_id NOT IN (SELECT Score.student_id FROM Score WHERE Score.course_id in (SELECT crs_id FROM Course LEFT JOIN Teacher ON Course.teacher_id = Teacher.tea_id WHERE tea_name LIKE '李平%'));
-- 物理成绩和生物成绩作两列才能比较;学号一致
SELECT Phy.student_id FROM (SELECT student_id, crs_name, num FROM Score LEFT JOIN Course ON Score.course_id = Course.crs_id WHERE crs_name LIKE '物理') AS Phy INNER JOIN (SELECT student_id, crs_name, num FROM Score LEFT JOIN Course ON Score.course_id = Course.crs_id WHERE crs_name LIKE '生物') AS Bio ON Phy.student_id = Bio.student_id WHERE Phy.num > Bio.num;
+------------+
| student_id | +------------+
| 6 |
| 7 |
| 8 | +------------+
3 rows in set (0.00 sec)
-- 找到同时选了的同学取反
SELECT std_name FROM Student WHERE std_id NOT IN (SELECT Phy.student_id FROM (SELECT student_id FROM Score LEFT JOIN Course ON Course.crs_id = Score.course_id WHERE crs_name LIKE '物理') AS Phy INNER JOIN (SELECT student_id FROM Score LEFT JOIN Course ON Course.crs_id = Score.course_id WHERE crs_name LIKE '体育') AS Gym ON Phy.student_id = Gym.student_id);
-- 因为是同时选了,所以Phy和Gym联结要用INNER JOIN, 否则会出现只选了一门的
+----------+
| std_name | +----------+
| 理解 |
| 钢蛋 |
| 刘三 |
| 刘一 |
| 刘二 |
| 刘四 | +----------+
6 rows in set (0.01 sec)
-- 筛选课程分数<60 按学生分组,计数
SELECT Student.std_name, Class.caption FROM Student LEFT JOIN Class ON Student.class_id = Class.cls_id WHERE Student.std_id IN (SELECT student_id FROM Score WHERE num < 60 GROUP BY student_id HAVING COUNT(*) >= 2);
+----------+----------+
| std_name | caption | +----------+----------+
| 理解 | 三年二班 | +----------+----------+
1 row in set (0.07 sec)
-- 学生分组 成绩记录等于课程数的
SELECT DISTINCT std_name -- 需要加 DISTICT 去重,否则每个名字出现多次 FROM Student INNER JOIN Score ON Student.std_id = Score.student_id WHERE Student.std_id IN (SELECT student_id FROM Score GROUP BY student_id HAVING COUNT(student_id) = (SELECT COUNT(crs_name) FROM Course));
+----------+
| std_name | +----------+
| 张三 |
| 张一 |
| 张二 |
| 张四 |
| 铁锤 |
| 李三 |
| 李一 |
| 李二 |
| 李四 |
| 如花 | +----------+
10 rows in set (0.00 sec)
SELECT num FROM Score WHERE course_id IN (SELECT Course.crs_id FROM Course INNER JOIN Teacher ON Course.teacher_id = Teacher.tea_id WHERE tea_name LIKE '李平%');
+-----+
| num | +-----+
| 9 |
| 66 |
| 11 |
| 11 |
-- ...
| 87 |
| 87 |
| 87 | +-----+
23 rows in set (0.08 sec)
-- 所有学生都选了的课程id:根据课程id分类计数,找出结果等于选课学生数的
SELECT crs_id, crs_name FROM Course INNER JOIN (SELECT course_id FROM Score GROUP BY course_id HAVING COUNT(course_id) = (SELECT COUNT(DISTINCT student_id) FROM Score)) AS Tmp ON Course.crs_id = Tmp.course_id;
Empty set (0.00 sec)
SELECT course_id, COUNT(course_id) FROM Score GROUP BY course_id;
+-----------+------------------+
| course_id | COUNT(course_id) | +-----------+------------------+
| 1 | 12 |
| 2 | 11 |
| 3 | 12 |
| 4 | 12 | +-----------+------------------+
4 rows in set (0.00 sec)
SELECT std_id, std_name FROM Student INNER JOIN (SELECT student_id FROM Score GROUP BY student_id HAVING COUNT(student_id)=1) AS Tmp ON Student.std_id = Tmp.student_id;
+--------+----------+
| std_id | std_name | +--------+----------+
| 13 | 刘三 | +--------+----------+
1 row in set (0.00 sec)
SELECT DISTINCT num FROM Score ORDER BY num DESC;
+-----+
| num | +-----+
| 100 |
| 99 |
-- ...
| 10 |
| 9 | +-----+
17 rows in set (0.00 sec)
SELECT std_name, AVG(num) FROM Student INNER JOIN Score ON Student.std_id = Score.student_id GROUP BY Student.std_id HAVING AVG(num) > 85;
+----------+----------+
| std_name | AVG(num) | +----------+----------+
| 刘三 | 87.0000 | +----------+----------+
1 row in set (0.00 sec)
SELECT std_name, num FROM Student INNER JOIN (SELECT student_id, num FROM Score INNER JOIN Course ON Score.course_id = Course.crs_id WHERE crs_name LIKE '生物' AND num < 60) AS Tmp ON Student.std_id = Tmp.student_id;
+----------+-----+
| std_name | num | +----------+-----+
| 理解 | 10 |
| 钢蛋 | 18 |
| 张四 | 9 |
| 铁锤 | 9 |
| 李三 | 9 | +----------+-----+
5 rows in set (0.00 sec)
-- 求最大值可以考虑用DESC 倒序
SELECT std_name FROM Student INNER JOIN (SELECT student_id FROM Score INNER JOIN (SELECT crs_id FROM Course INNER JOIN Teacher ON Course.teacher_id = Teacher.tea_id WHERE tea_name LIKE '李平%') AS Tmp ON Score.course_id = Tmp.crs_id GROUP BY student_id ORDER BY AVG(num) DESC LIMIT 1) AS RES ON Student.std_id = RES.student_id;
+----------+
| std_name | +----------+
| 张四 | +----------+
1 row in set (0.00 sec)
-- 将SELECT结果作为字段名; LIMIT m, n 取出第二高的分数
SELECT * FROM Score LEFT JOIN (SELECT scr_id, (SELECT num FROM Score AS s2 WHERE s2.course_id = s1.course_id ORDER BY num DESC LIMIT 1) AS first_num, (SELECT DISTINCT num FROM Score AS s2 WHERE S2.course_id = s1.course_id ORDER BY num DESC LIMIT 1,1) AS second_num FROM score as s1) AS T ON Score.scr_id = T.scr_id WHERE Score.num <= T.first_num AND Score.num >= T.second_num;
-- 自联结
SELECT A.student_id, A.course_id, A.num
FROM Score AS A, Score AS B
WHERE A.course_id != B.course_id
AND A.student_id = B.student_id
AND A.num = B.num;
+------------+-----------+-----+
| student_id | course_id | num | +------------+-----------+-----+
| 6 | 2 | 100 |
| 6 | 4 | 100 | +------------+-----------+-----+
2 rows in set (0.00 sec)
SELECT std_name, crs_name, tea_name FROM Student INNER JOIN (SELECT student_id, crs_name, tea_name FROM Score INNER JOIN (SELECT crs_id, crs_name, tea_name FROM Course INNER JOIN (SELECT tea_id, tea_name FROM Teacher WHERE tea_name NOT LIKE '李平%') AS T ON Course.teacher_id = T.tea_id) AS C ON Score.course_id = C.crs_id) AS S ON Student.std_id = S.student_id ORDER BY std_name;
+----------+----------+------------+
| std_name | crs_name | tea_name | +----------+----------+------------+
| 刘三 | 体育 | 刘海燕老师 |
| 如花 | 体育 | 刘海燕老师 |
| 如花 | 生物 | 张磊老师 |
-- ...
| 铁锤 | 生物 | 张磊老师 |
| 铁锤 | 体育 | 刘海燕老师 | +----------+----------+------------+
24 rows in set (0.00 sec)
-- 任课最多的老师课程id下的学生
SELECT std_name, num, crs_name FROM Student INNER JOIN (SELECT student_id, num, crs_name FROM Score INNER JOIN (SELECT crs_id, crs_name FROM Course INNER JOIN (SELECT teacher_id FROM course GROUP BY teacher_id ORDER BY COUNT(teacher_id) DESC LIMIT 1) AS T ON Course.teacher_id = T.teacher_id) AS C ON Score.course_id = C.crs_id) AS S ON Student.std_id = S.student_id ORDER BY num DESC LIMIT 8; -- 数据问题,同时有很多最高的。。。
+----------+-----+----------+
| std_name | num | crs_name | +----------+-----+----------+
| 李三 | 100 | 物理 |
| 张四 | 100 | 美术 |
| 铁锤 | 100 | 物理 |
| 张二 | 100 | 美术 |
| 张四 | 100 | 物理 |
| 张一 | 100 | 美术 |
| 张三 | 99 | 美术 |
| 钢蛋 | 99 | 美术 | +----------+-----+----------+
8 rows in set (0.00 sec)