数据库基本概念
1. SQL 定义
SQL 是 Structured-Query-Language(结构化查询语句)的缩写。SQL 是由 ANSI 标准委员会制定的,所有主要的 DBMS 都是在 SQL 基础上扩展一些执行特定操作的额外功能或简化方法。
2. 数据库分类
- 关系型数据库:用来存放实体与实体之间关系的数据库。(
mysql
,orcale
) - 非关系型数据库:存放的是对象。(
radis
,mongdb
,hbase
)。
3. SQL 中的一些基本概念
-
库:数据库表示以某种有组织的方式存储的数据集合。库类似于文件夹,本身没法存数据。
-
表:是一种结构化的文件,可以用来存储某种特定类型的数据,类似于 excel 文件。数据库中的每一个表都用一个表名来标识。在同一个库中,表名是唯一的,通过库名和表名来标识一个表。
-
列:表中的一个字段。所有表都是由一个或多个列组成的。
-
数据类型:每一列都有相应的数据类型。MySQL 中常用的数据类型有:
mysql java tinyint byte smallint short int(常用) int bigint long varchar()|char(常用) char/string float/double float/double date java.sql.Date datetime(常用) 日期+时间 timestamp java.sql.Timestamp(常用) text 是 mysql 的方言 java.sql.Clob (长文本,如小说) blob java.sql.Blob (存放二进制大对象,如电影) 数据类型及其名称是 SQL 不兼容的一个主要原因。虽然大多数基本数据类型得到了一致的支持,但许多高级的数据类型却没有。此外,存在相同的数据类型在不同的 DBMS 中具有不同的名称。
-
行:又称记录,表中的数据是按行存储的,每条记录存储为一行。
-
主键:表中的一列或几列,其值用于唯一标识表中的每一行。这一列或几列的组合称为主键。如顾客表中的顾客编号,订单表中的订单 ID。表中可以没有主键,但是缺少主键,更新或删除表中特定行就极为困难,因为你不能保证操作只涉及相关的行。
表中的任何列都可以作为主键,只要它满足以下几个条件:
- 任意两行都不具有相同的主键值。
- 每一行都必须具有一个主键值(主键列不能为 NULL)。
- 主键列的值不允许修改或更新。
- 主键值不能重用(如果某行从表中删除,它的主键不能赋给以后的新行)?
通常使用某一列作为主键,也可以使用多个列作为主键。在使用多列作为主键时,上述条件必须应用到所有列,把多个列值的组合视为主键。所有列值的组合必须是唯一的(单个列的值可以不唯一)。
4. 安装 MySql
安装 mysql 时,只需安装 server 端。安装完成后,需要将 mysql.exe (在bin目录下)所在的绝对路径,放入环境变量中。mySQL 安装时,需要注意验证选择,不要选择强密码校验。
启动并进入数据库环境:
-
启动数据库服务:在
win + r
中输入services.msc
,然后在弹出的窗口中找到 mysql80,确保其已启动,否则,将其设置为启动。或者在命令行中输入net start mysql
。 -
然后输入命令
mysql -uroot -p密码
登录数据库。 -
本笔记使用的样例数据:[http://forta.com/books/0672336073]
5. MySql 中的注释语法:
- "-- ":两个斜杠一个空格,所在行后面跟随的文本为注释。
- "#":一个 #。也是行内注释。
- "/* ... */":多行注释。
6. 数据库操作
数据库的4大操作:增删改查,简称 curd:
- c(create):增加
- d(delete):删除
- u(update):修改
- r(read):查询
数据库本质上是一个文件系统。通过标准的 SQL 语句对数据进行 curd 操作。
查询语句
1. 检索数据
SQL 语句是由简单的英语单词构成的。这些单词称为关键字,每个 SQL 语句都是由一个或多个关键字构成的。最常使用的 SQL 语句就是 select 语句,它用于从一个或多个表中检索信息。
1.1 检索单个字段
语句用法如下:
select prod_name from Products;
上述语句用于从表 Products 表中检索名为 prod_name 的字段。待检索的字段名写在 select 关键字之后,from 关键字指出从哪个表中检索数据。语句输出如下:
+---------------------+
| prod_name |
+---------------------+
| 8 inch teddy bear |
| 12 inch teddy bear |
| 18 inch teddy bear |
| Fish bean bag toy |
| Bird bean bag toy |
| Rabbit bean bag toy |
| Raggedy Ann |
| King doll |
| Queen doll |
+---------------------+
结束 SQL 语句
多条 SQL 语句必须以分号(;)分隔。多数 DBMS 不需要在单条 SQL 语句后加分号,但最好加上分号。
SQL 语句和大小写
SQL 语句不区分大小写,因此 SELECT 和 select 是相同的。许多 SQL 开发人员喜欢对 SQL 关键字使用大写,而对列名和表名使用小写。不过,要认识到虽然 SQL 不区分大小写,但是表名、列名和值是否区分大小写取决于具体的 DBMS。所以最好关键字用大写,表名、字段名和字段值按照区分大小写使用。
使用空格
在处理 SQL 语句时,其中所有空格被忽略,SQL 语句可以写成很长的一行,也可以分写在多行。
1.2 检索多个列
要想从一个表中检索多个列,仍然使用相同的 select 语句。唯一的不同是必须在 select 关键字后给出多个列名,列名之间必须以逗号分隔。
在选择多个列时一定要在列名之间加上逗号,但最后一个列名后不加逗号。如果在最后一个列名后面加上逗号,将出现错误。
示例如下:
输入:
select prod_id, prod_name, prod_price from products;
输出:
+---------+---------------------+------------+
| prod_id | prod_name | prod_price |
+---------+---------------------+------------+
| BR01 | 8 inch teddy bear | 5.99 |
| BR02 | 12 inch teddy bear | 8.99 |
| BR03 | 18 inch teddy bear | 11.99 |
| BNBG01 | Fish bean bag toy | 3.49 |
| BNBG02 | Bird bean bag toy | 3.49 |
| BNBG03 | Rabbit bean bag toy | 3.49 |
| RGAN01 | Raggedy Ann | 4.99 |
| RYL01 | King doll | 9.49 |
| RYL02 | Queen doll | 9.49 |
+---------+---------------------+------------+
1.3 检索所有列
select 语句还可以检索所有的列,而不必逐个列出字段名。在实际字段名的位置使用星号(*)通配符来实现,示例如下。字段的顺序一般是字段在表中出现的物理顺序,但并不总是如此。
输入:
select * from products;
输出:
+---------+---------+---------------------+-----------+
| prod_id | vend_id | prod_name | prod_price|
+---------+---------+---------------------+-----------|
| BR01 | BRS01 | 8 inch teddy bear | 5.99|
| BR02 | BRS01 | 12 inch teddy bear | 8.99|
| BR03 | BRS01 | 18 inch teddy bear | 11.99|
| BNBG01 | DLL01 | Fish bean bag toy | 3.49|
| BNBG02 | DLL01 | Bird bean bag toy | 3.49|
| BNBG03 | DLL01 | Rabbit bean bag toy | 3.49|
| RGAN01 | DLL01 | Raggedy Ann | 4.99|
| RYL01 | FNG01 | King doll | 9.49|
| RYL02 | FNG01 | Queen doll | 9.4 |
+---------+---------+---------------------+-----------+
1.4 检索不同的值
使用 select 语句检索某字段时,会返回所有行的该字段值。其中,可能存在某些行,该字段的值相同。例如:
输入:
select vend_id fom products;
输出:
+---------+
| vend_id |
+---------+
| BRS01 |
| BRS01 |
| BRS01 |
| DLL01 |
| DLL01 |
| DLL01 |
| DLL01 |
| FNG01 |
| FNG01 |
+---------+
如果想要去除重复值,只输出字段的不同值。可在字段名前添加关键字 distinct。
输入:
select distinct vend_id fom products;
输出:
+---------+
| vend_id |
+---------+
| BRS01 |
| DLL01 |
| FNG01 |
+---------+
不能部分使用 distinct
distinct 关键字作用于所有的列,不仅仅是跟在其后的那一列。此时,只有当多个字段值的组合完全相同时,才会去掉重复。例如:
输入:
select vend_id, prod_price from products;
输出如下:
+---------+------------+
| vend_id | prod_price |
+---------+------------+
| BRS01 | 5.99 |
| BRS01 | 8.99 |
| BRS01 | 11.99 |
| DLL01 | 3.49 |
| DLL01 | 3.49 |
| DLL01 | 3.49 |
| DLL01 | 4.99 |
| FNG01 | 9.49 |
| FNG01 | 9.49 |
+---------+------------+
输入:
select distinct vend_id, prod_price from products;
输出:
+---------+------------+
| vend_id | prod_price |
+---------+------------+
| BRS01 | 5.99 |
| BRS01 | 8.99 |
| BRS01 | 11.99 |
| DLL01 | 3.49 |
| DLL01 | 4.99 |
| FNG01 | 9.49 |
+---------+------------+
1.5 限制结果
select 语句会返回满足条件的所有行。如果希望 select 语句只返回第一行或一定数量的行,则需要对 select 语句添加限制条件,此限制条件的语法在不同的 DBMS 中不同。MySQL 中使用 limit 和 offset 关键字进行限定。示例如下:
select prod_name from products limit 5;
limit 5
表示至多返回前5行数据。如果需要返回从第 i 行开始的 j 行数据,需要再使用 offset 关键字:
select prod_name from products limit i offset j;
offset j
表示从第 j 行开始取数据,j 从 0 开始取值,j = 0
表示第一行,limit i
表示取的数据个数。
上述语句可简写为:
select prod_name from products limit j,i;
limit 后第一个参数对应于 offset 后的值,表示起始位置,第二个参数对应 limit 后的值,表示取的数据个数。
2. 排序检索数据
本节介绍利用 select 语句的 order by 子句,根据需要排序检索出的数据。
子句
SQL 语句由子句构成,有些子句是必需的。有些则是可选的。一个子句通常由一个关键字加上所提供的数据组成。
2.1 排序数据
select 语句可以查询出指定字段的值,但是给出的查询结果未排序。在不同的 DBMS中,显示的顺序可能不同。如果对查询的结果进行排序,则输出结果的顺序必定一致,SQL 中使用 order by 子句对数据进行排序,order by 后面添加一个或多个字段名,表明依据这些字段的值对查询结果进行排序。示例如下:
select prod_name from products order by prod_name;
order by 子句的位置
在指定一条 order by 子句时,应该保证它是 select 语句的最后一条子句。如果它不是最后的子句,将会出现错误。
使用非选择列进行排序
通常 order by 子句中使用的列是待查询的字段。但是,也可以使用非待查询字段排序数据。
2.2 按多个列排序
当需要按多个列进行排序时,只需要在 order by 后列出多个字段,字段之间用逗号分隔。SQL 会先按第一个关键字进行排序,第一个关键字相同的,按第二、三……关键字进行排序。
2.3 按列位置排序
除了能用列名指出排序顺序外,order by 还支持按 select 语句中待查询字段的相对次序进行排序。例如:
输入:
select prod_id, prod_price, prod_name from products order by 2, 3;
输出:
+---------+------------+---------------------+
| prod_id | prod_price | prod_name |
+---------+------------+---------------------+
| BNBG02 | 3.49 | Bird bean bag toy |
| BNBG01 | 3.49 | Fish bean bag toy |
| BNBG03 | 3.49 | Rabbit bean bag toy |
| RGAN01 | 4.99 | Raggedy Ann |
| BR01 | 5.99 | 8 inch teddy bear |
| BR02 | 8.99 | 12 inch teddy bear |
| RYL01 | 9.49 | King doll |
| RYL02 | 9.49 | Queen doll |
| BR03 | 11.99 | 18 inch teddy bear |
+---------+------------+---------------------+
order by 2
表示按 select 语句中的第二个字段 prod_price 进行排序。order by 3
表示按 select 语句中的第三个字段 prod_name 进行排序。可以看出,select 语句中字段次序从1开始。
此种用法的优点是:无需重新输入列名。
缺点是:
- 不明确给出列名有可能造成错用列名排序。
- 在对 select 清单进行更改时容易忘记对 order by 子句进行相应地改动,错误地对数据进行排序。
- 当进行排序的字段不在 select 清单中时,无法使用该语句。
2.4 指定排序方向
order by 子句默认采用升序排序。为了进行降序排序,必须指定 desc 关键字。示例如下:
输入:
select prod_id, prod_price, prod_name from products order by prod_price desc;
输出:
+---------+------------+---------------------+
| prod_id | prod_price | prod_name |
+---------+------------+---------------------+
| BR03 | 11.99 | 18 inch teddy bear |
| RYL01 | 9.49 | King doll |
| RYL02 | 9.49 | Queen doll |
| BR02 | 8.99 | 12 inch teddy bear |
| BR01 | 5.99 | 8 inch teddy bear |
| RGAN01 | 4.99 | Raggedy Ann |
| BNBG01 | 3.49 | Fish bean bag toy |
| BNBG02 | 3.49 | Bird bean bag toy |
| BNBG03 | 3.49 | Rabbit bean bag toy |
+---------+------------+---------------------+
当需要对多个字段排序时,desc 只作用于直接位于其前面的字段。例如:
输入:
select prod_id, prod_price, prod_name from products order by prod_price desc, prod_name;
输出:
+---------+------------+---------------------+
| prod_id | prod_price | prod_name |
+---------+------------+---------------------+
| BR03 | 11.99 | 18 inch teddy bear |
| RYL01 | 9.49 | King doll |
| RYL02 | 9.49 | Queen doll |
| BR02 | 8.99 | 12 inch teddy bear |
| BR01 | 5.99 | 8 inch teddy bear |
| RGAN01 | 4.99 | Raggedy Ann |
| BNBG02 | 3.49 | Bird bean bag toy |
| BNBG01 | 3.49 | Fish bean bag toy |
| BNBG03 | 3.49 | Rabbit bean bag toy |
+---------+------------+---------------------+
上面的查询语句只对 prod_price 指定 desc,对 prod_name 不指定 desc,所以对 prod_price 列降序排序,而 prod_name 仍升序排序。
警告:在多个列上降序排序
如果想对多个字段进行降序排序,必须对每一个字段指定 desc 关键字。
字符串排序时字符的大小写问题
在对字符串排序时,是否区分大小写,即 A 和 a 是否相等,取决于数据库的设置。默认情况下,多数数据库系统不区分大小写(如 MySQL),但是会允许程序员设定为区分大小写。
3. 过滤数据
3.1 使用 where 子句
如果只检索某些行而不是所有行,那么需要对数据进行过滤。在 select 语句中,通过在 where 子句中给定搜索条件进行过滤。where 子句在表名(from 子句)之后给出。示例如下:
输入:
select prod_name, prod_price from products where prod_price = 3.49;
输出:
+---------------------+------------+
| prod_name | prod_price |
+---------------------+------------+
| Fish bean bag toy | 3.49 |
| Bird bean bag toy | 3.49 |
| Rabbit bean bag toy | 3.49 |
+---------------------+------------+
上述语句从 products 表中检索两列,但不返回所有行,只返回 prod_price = 3.9 的行。
where 子句的位置
在同时使用 order by 和 where 子句时,应该让 order by 位于 where 子句之后。否则,会报错。表明先过滤数据,然后在排序
3.2 where 子句操作符
SQL 支持的条件操作符如下表:
操作符 | 说明 | 操作符 | 说明 |
---|---|---|---|
= | 等于 | > | 大于 |
<> | 不等 | >= | 大于等于 |
!= | 不等 | !> | 不大于 |
< | 小于 | between and | 在指定的两个值之间,包含两个端点,用于检查范围值 |
<= | 小于等于 | is null | 为 null 值 |
!< | 不小于 |
表中列出的某些操作符是冗余的,并非所有 DBMS 都支持这些操作符。需要查看具体的 DBMS 的语法。
常见操作符的用法如下:
select prod_name, vend_id from products where vend_id <> 'DLL01';
select prod_name, vend_id from products where vend_id != 'DLL01';
# 范围检查,例如:检索价格介于 5~10之间的所有商品
select prod_name, prod_price from products where prod_price between 5 and 10;
输出:
+--------------------+------------+
| prod_name | prod_price |
+--------------------+------------+
| 8 inch teddy bear | 5.99 |
| 12 inch teddy bear | 8.99 |
| King doll | 9.49 |
| Queen doll | 9.49 |
+--------------------+------------+
# 空值检查
select prod_name from products where prod_price is null;
# 非空检查
select prod_name from products where prod_price is not null;
在使用 between 检查范围时,必须指定两个值 ——所需范围的低端值和高端值。这两个值必须用 and 关键字分隔。between 匹配范围中所有的值,包括指定的开始值和结束值。
在创建表时,我们可以指定每个字段能否不包含值。在一个字段不包含值时,称其包含空值 null。空值与字段值为0、空字符串或仅仅包含空格不同,它表明本记录该字段值缺失。判断字段值是否 null,不能简单地检查是否 = null。而需使用 where 的 is null 操作符。
null 和非匹配
查找不包含给定字段值的所有记录时,也会排除掉字段值为 null的记录。因为未知有特殊的含义,数据库不知道它们是否匹配,所以在进行匹配过滤或非匹配过滤时,都会排除掉空值的记录。
在过滤数据时,一定要验证过滤列中含 null 的行确实出现在返回的数据中。
4. 高级数据过滤
4.1 组合 where 子句
为了进行更强的过滤控制,SQL 允许在 where 子句中使用 and 和 or 操作符组合多个过滤条件。示例如下:
select prod_price, vend_id from products where vend_id = 'DLL01' and prod_price <= 4;
select prod_Name, prof_price from products where vend_id = 'DLL01' or vend_id = 'BRs01';
运算符的优先级
当 where 子句中同时含有 and 和 or 操作符时, and 操作符的优先级比 or 操作符优先级高。
4.2 in 操作符
in 操作符的作用和 or 操作符类似,用于指定条件范围。in 操作符取一组由逗号分隔、括在圆括号中的合法值。示例如下:
select prod_name, prod_price from products where vend_id in ('DLL01', 'BRS01') order by prod_name;
上述语句等价于:
select prod_name, prod_price from products where vend_id = 'DLL01' or vend_id = 'BRS01' order by prod_name;
相比 or 操作符,in 操作符的优点是:
- 在有很多合法选项时,in 操作符的语法更清楚。
- in 操作符一般比一组 or 操作符执行的更快。
- in 操作符的最大优点是可以包含其他 select 语句。
4.3 not 操作符
where 子句中的 not 操作符有且只有一个功能,那就是否定其后所跟的任何条件。not 的优先级高于 and 和 or。示例如下:
输入:
select * from orders;
输出:
+-----------+---------------------+------------+
| order_num | order_date | cust_id |
+-----------+---------------------+------------+
| 20005 | 2012-05-01 00:00:00 | 1000000001 |
| 20006 | 2012-01-12 00:00:00 | 1000000003 |
| 20007 | 2012-01-30 00:00:00 | 1000000004 |
| 20008 | 2012-02-03 00:00:00 | 1000000005 |
| 20009 | 2012-02-08 00:00:00 | 1000000001 |
+-----------+---------------------+------------+
输入:
select * from orders where not order_num = 20005 or order_num = 20005;
输出:
+-----------+---------------------+------------+
| order_num | order_date | cust_id |
+-----------+---------------------+------------+
| 20005 | 2012-05-01 00:00:00 | 1000000001 |
| 20006 | 2012-01-12 00:00:00 | 1000000003 |
| 20007 | 2012-01-30 00:00:00 | 1000000004 |
| 20008 | 2012-02-03 00:00:00 | 1000000005 |
| 20009 | 2012-02-08 00:00:00 | 1000000001 |
+-----------+---------------------+------------+
5. 用通配符进行过滤
5.1 like 操作符
通配符是 SQL 的 where 子句中有特殊含义的字符,SQL 支持集中通配符,为在搜索子句中使用通配符,必须使用 like 操作符。like 指示 DBMS,后跟的搜索模式利用通配符匹配而不是简单的相等匹配进行。通配符搜索只能用于字符串,非文本数据类型字段
5.1.1 百分号(%)通配符
在搜索串中,%表示任何字符出现任意次数(长度可为0),但不能匹配 null
。如果使用的是 Microsoft Access
,需要使用 *
而不是 %
。示例如下:
输入:
select prod_id, prod_name from products where prod_name like 'fish%';
输出:
+---------+-------------------+
| prod_id | prod_name |
+---------+-------------------+
| BNBG01 | Fish bean bag toy |
+---------+-------------------+
匹配字符串时大小写问题
根据数据库系统及其配置的不同,搜索可以是区分大小写的
5.1.2 下划线(_)通配符
下划线通配符只匹配任意的单个字符。如果使用的是 Microsoft Access,需要使用?而不是 _ 。示例如下:
输入:
select prod_id, prod_name from products where prod_name like '_ inch teddy bear';
输出:
+---------+-------------------+
| prod_id | prod_name |
+---------+-------------------+
| BR01 | 8 inch teddy bear |
+---------+-------------------+
5.1.3 方括号([])通配符
方括号([])通配符用来指定一个字符集合,字符串中待匹配的字符必须在字符集合中,才匹配成功。MySQL 中不支持。例如:select * from product where name like '[jm]%'
。表示选出所有名称以 j,m
开头的商品
[^] 通配符:作用和方括号通配符相反,表示在方括号位置不匹配方括号中的任意字符。当然,也可以在方括号通配符中使用 not 实现相同的功能。例如:select * from product where name like '[^jm]%'
。表示选出所有名称不以 j,m
开头的商品。
5.2 使用通配符的技巧
SQL 的通配符很有用,但这种功能是有代价的,即通配符搜索一般比前面讨论的其他搜索要耗费更长的处理时间。在使用通配符时,注意以下技巧:
- 不要过度使用通配符。如果其他操作符能达到相同的目的,应该使用其他操作符。
- 在缺失需要使用通配符时,也尽量不要把它们用在搜索模式的开始处。把通配符置于开始处,搜索起来是最慢的。
- 仔细注意通配符的位置。如果放错了位置,可能不会返回想要的数据。
通配符可在搜索模式中的任意位置使用,且可以使用多个通配符。
6. 创建计算字段
6.1 计算字段
存储在表中的数据一般不是应用程序所需要的。我们需要对从数据库中检索出的数据进行转换、计算或格式化,而不是检索出数据,然后在客户端应用程序中重新格式化。此时需要使用计算字段,计算字段并不实际存在于数据表中。计算字段是运行时在 select 语句内创建的。
客户端与服务器的格式
在 SQL 语句内可完成的许多转换和格式化工作都可以直接在客户端应用程序内完成。但一般来说,在数据库服务器上完成这些操作比在客户端中完成要快很多。
6.2 拼接字段
拼接即为:通过加号+或者两个竖杠将多个字段值或字符串拼接成一个字段。不同的 DBMS 中实现不同,如:
- Access 和 SQL Server 使用 + 号。
- DB2、Oracle、SQLite使用 ||。
- MySQL 和 Maria DB 使用 concat 和 concat_ws。concat(字段1或字符串1,字段2或字符串2,……)。concat_ws 可以指定分隔符,concat_ws(分隔符,字段1或字符串1,字段2或字符串2,……)。示例如下
输入:
select concat(prod_id, ' ', prod_price) from products;
输出:
+----------------------------------+
| concat(prod_id, ' ', prod_price) |
+----------------------------------+
| BR01 5.99 |
| BR02 8.99 |
| BR03 11.99 |
| BNBG01 3.49 |
| BNBG02 3.49 |
| BNBG03 3.49 |
| RGAN01 4.99 |
| RYL01 9.49 |
| RYL02 9.49 |
+----------------------------------+
输入:
select concat_ws(',', prod_id, prod_price) from products;
输出:
+-------------------------------------+
| concat_ws(',', prod_id, prod_price) |
+-------------------------------------+
| BR01,5.99 |
| BR02,8.99 |
| BR03,11.99 |
| BNBG01,3.49 |
| BNBG02,3.49 |
| BNBG03,3.49 |
| RGAN01,4.99 |
| RYL01,9.49 |
| RYL02,9.49 |
+-------------------------------------+
6.2.1 去除空格
许多数据库保存填充为列宽的文本值(即文本长度小于列宽时,用空格填充)。在进行字段拼接时,某些 DBMS 中可能会出现两个字段之间很长的空格。此时再拼接时,需要去除这些填充字段。SQL 使用 RTRIM() 函数来完成,用法为:select rtrim(vend_name) + '(' + rtrim(vend_country) + ')' from vendors;
函数功能说明
rtrim()
:去掉值右边的所有空格。ltrim()
:去掉值左边的所有空格。trim()
:去掉值两边的所有空格。
6.2.2 使用别名
在 select 中进行字段拼接时,可以使用 as 关键字给拼接后的字段起一个别名。任何客户端应用都可以按名称引用这个列,就像它是一个实际的表一样。示例如下:
输入:
select concat(prod_id, ' ', prod_price) as alias from products;
输出:
+-------------+
| alias |
+-------------+
| BR01 5.99 |
| BR02 8.99 |
| BR03 11.99 |
| BNBG01 3.49 |
| BNBG02 3.49 |
| BNBG03 3.49 |
| RGAN01 4.99 |
| RYL01 9.49 |
| RYL02 9.49 |
+-------------+
别名既可以是一个单词,也可以是一个字符串。如果是后者,字符串应该括在引号中。多单词的名字可读性高,不过会给客户端应用带来各种问题,因此不建议这么做。通常命名为一个单词。
6.3 执行算术计算
计算字段的另一常见用途是对检索出的数据进行算术计算。计算字段只支持加、减、乘和除四种基本运算,以及小括号。
输入:
select quantity, item_price, quantity*item_price as expand from orderitems where quantity > 100;
输出:
+----------+------------+--------+
| quantity | item_price | expand |
+----------+------------+--------+
| 250 | 2.49 | 622.50 |
| 250 | 2.49 | 622.50 |
| 250 | 2.49 | 622.50 |
+----------+------------+--------+
注:
为什么select quantity, item_price, quantity*item_price as expanded_price from orderitems where expanded_price > 100; 别名不能用在where后面。
7. 使用函数处理数据
7.1 函数的不可移植性
几乎所有的 DBMS 都支持相同的 SQL 语句,但是每个 DBMS 支持的函数各不相同,事实上,只有少数几个函数是所有主要的 DBMS 都支持的。所以,SQL 中的函数不可移植。
大部分 SQL 实现支持以下类型的函数:
- 用于处理文本字符串(如删除或填充值,转换值为大写或小写)的文本函数,如 RTRIM()。
- 用于在数值数据上进行算术操作(如返回绝对值,进行代数运算)的数值函数。
- 用于处理日期和时间值,并从这些值中提取特定成分(如返回两个日期之差,检查日期有效性)的日期和时间函数。
- 返回 DBMS 正使用的特殊信息(如返回用户登录信息)的系统函数。
7.2 文本处理函数
常用的处理函数有:
left()
: 返回字符串左边的字符。right()
:返回字符串右边的字符。length()
:返回字符串的长度。lower()
:将字符串转换为小写。upper()
:将字符串转换为大写。ltrim()
、rtrim()
、trim()
:去除字符串的空格。
7.3 日期和时间处理函数
在不同的 DBMS 中,日期和时间值存储格式各不相同,以便能快速和有效地排序或过滤,并且节省物理存储空间。应用程序一般不使用日期和时间的存储格式,通常借助于日期和时间函数来读取、统计、处理日期和时间。示例,为了获得2012年的订单记录,需要使用如下命令:
输入:
select * from orders where year(order_date) = 2012;
输出:
+-----------+---------------------+------------+
| order_num | order_date | cust_id |
+-----------+---------------------+------------+
| 20005 | 2012-05-01 00:00:00 | 1000000001 |
| 20006 | 2012-01-12 00:00:00 | 1000000003 |
| 20007 | 2012-01-30 00:00:00 | 1000000004 |
| 20008 | 2012-02-03 00:00:00 | 1000000005 |
| 20009 | 2012-02-08 00:00:00 | 1000000001 |
+-----------+---------------------+------------+
7.4 数值处理函数
数值处理函数仅处理数值数据。这些函数一般主要用于代数、三角或几何运算,不像字符串或日期-时间处理函数使用那么频繁。各 DBMS 的数值函数实现形式基本相同。数据库常用的数值处理函数有:abs()、cos()、exp()、pi()、sin()、sqrt()、tan()。
8. 汇总数据
8.1 聚集函数
我们经常需要汇总表中数据,而不需要检索出实际数据,因此返回实际表数据纯属浪费时间和CPU。
聚集函数
对某些行运行的函数,计算并返回一个值。
SQL 提供了专门的聚集函数。使用这些函数,SQL查询可用于检索数据,以便分析和报表生成。这种类型的检索例子有:
- 确定表中行数(或者满足某个条件或包含某个特定值的行数);
- 获得表中某些行的和;
- 找出表列(或所有行或某些特定的行)的最大值、最小值、平均值。
SQL 提供了5个聚集函数:
函数 | 说明 |
---|---|
AVG() | 返回某列的平均值 |
COUNT() | 返回某列的行数 |
MAX() | 返回某列的最大值 |
MIN() | 返回某列的最小值 |
SUM() | 返回某列值之和 |
8.1.1 AVG()函数
AVG() 通过对表中行数技术并计算其列值之和,求得该列的平均值。AVG() 可用来返回所有列的平均值,也可以用来返回特定列或行的平均值。示例如下:
select avg(prod_price) as avg_price from Products where vend_id = 'DLL01';
注意:只用于单个列
AVG() 只能用于确定特定数值列的平均值,而且列名必须作为函数参数给出。为了获得多个列的平均值,必须使用多个AVG()函数。
说明:NULL值
AVG()函数忽略列值为 null 的行。仅忽略参数值为 null 的行,当所在行其他字段的值为 null 时,不忽略
8.1.2 COUNT() 函数
COUNT() 函数进行技术。可利用 COUNT() 确定表中行的数目或符合特定条件的行的数目。COUNT() 函数有两种使用方式:
- 使用 COUNT(*) 对表中行的数目进行计数,不管表列中包含的是空值(null)还是非空值。
- 使用 COUNT(column) 对特定列中具有值的行进行技术,忽略 null 值。
8.1.3 MAX() 和 MIN()函数
MAX() 和 MIN() 函数返回指定列的最大/小值。MAX() 和 MIN() 要求指定列名。示例如下:
select max(prod_price)/min(prod_price) as max_price/min_price fom products;
提示:对非数值数据使用MAX() 和 MIN()
虽然 MAX() 和 MIN() 一般用来找出最大/小的数值或日期值,但许多 DBMS 允许将它用来返回任意列中的最大值,包括返回文本列中的最大/小值。在用于文本数据时,MAX() 和 MIN() 返回按该列排序后的最后一列。
说明:忽略 null 值
MAX() 和 MIN() 函数忽略列值为 null 的行
8.1.4 SUM() 函数
SUM() 函数返回指定列值的和,用法如下:
select sum(item_price*quantity) as total_price from OrderItems where order_num = 20005;
提示:在多个列上进行计算
如本例所示,利用标准的算术操作符,所有聚集函数都可用来执行多个列上的计算。
说明:NULL 值
SUM() 函数忽略列值为 null 的行。
8.2 聚集不同值
以上5个聚集函数都可以如下使用:
- 对特定字段的所有行执行计算,在字段名前指定 ALL 参数或不指定参数。
- 只包含不同的值,指定 DISTINCT 参数。
示例如下:
select count(distinct order_item * item_price) from orderitems;
注意:
- distinct 不能用于 count(*)
- distinct 可以用于表达式
- 不同的 DBMS 还存在其他聚集函数
8.3 组合聚集函数
目前为止的所有聚集函数例子都只涉及单个函数。但实际上,select 语句可根据需要包含多个聚集函数。示例如下:
select count(*) as num_items, min(prod_price) as price_min, max(prod_price) as price_max, avg(prod_price) as avg_price from Products;
注意:取别名
在指定别名以包含某个聚集函数的结果时,不应该使用表中实际的列名。虽然这样做也算合法,但许多 SQL 实现不支持。可能会产生模糊的错误消息。
9. 分组数据
9.1 创建分组
分组是使用 select 语句的 group by 子句建立的。示例如下:
输入:
select vend_id, count(*) as num_prod from Products group by vend_id;
输出:
+---------+----------+
| vend_id | num_prod |
+---------+----------+
| BRS01 | 3 |
| DLL01 | 4 |
| FNG01 | 2 |
+---------+----------+
group by 子句用于按 vend_id 字段对 Products 表进行分组,count(*) 表示统计每个分组包含的记录数。
在使用 group by 子句前,需要知道一些重要的规定:
- group by 子句可以包含任意数目的字段,因而可以对分组进行嵌套,更细致地进行数据分组。
- 如果在 group by 子句中嵌套分组(使用多个字段),那么,在建立分组时,按照所有字段值的组合进行分组,只有当组合值相同时,才视为同一个分组。
- group by 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 select 中使用表达式,则必须在 group by 子句中使用相同的表达式。不能使用别名。
- 除聚集计算语句外,select 语句中的每一个字段都必须在 group by 子句中给出。若 select 语句中使用了表达式,那么,group by 子句中必须有相同的表达式。
- 大多数 sql 实现不允许 group by 子句使用长度可变的数据类型。
- 如果分组中包含具有 null 值的行,则 null 将作为一个分组返回。如果列中有多行 null 值,它们将分为一组。
- group by 子句必须出现在 where 子句之后,order by 子句之前。
使用 group by 子句后,整个表变为几个分组,select 语句仅能使用 group by 子句中的存在的字段/表达式,或聚集函数。含有 group by 子句的语句常用于分组使用聚集函数。
9.2 过滤分组
除了能用 group by 分组数据外,sql 还允许过滤分组,规定包括哪些分组,排除哪些分组。过滤分组使用 having 子句。
having 子句的示例如下:
输入:
select cust_id, count(*) as orders from orders group by cust_id having count(*) > 1;
输出:
+------------+--------+
| cust_id | orders |
+------------+--------+
| 1000000001 | 2 |
+------------+--------+
作用:选出至少有两个订单的顾客
having 和 where 的差别
having 类似于 where 子句,支持 where by 子句的所有操作符。但是 where 用于过滤行。having 用于过滤分组。在没有 group by 子句时,相当于默认每一行是一个分组,此时 having 子句的作用是过滤行(使用较少)。
另一种理解方法是:where 在数据分组前过滤行,having 在数据分组后过滤分组。where 排除的行不包括在分组中,从而影响 having 子句中基于这些值过滤掉的分组。
9.2.1 同时使用 where 子句 和 having 子句
示例如下:
输入:
select vend_id from products where prod_price > 4 group by vend_id having count(*) > 1;
输出:
+---------+
| vend_id |
+---------+
| BRS01 |
| FNG01 |
+---------+
说明:输出有两件产品价格大于4的订单的供应商。
分析:where prod_price > 4 用于排除 products 表中 prod_price 小于4的记录。然后对过滤后的记录进行按 vend_id 分组,并统计每个组的记录个数,having count(*) > 1,用于过滤掉记录数小于2的分组。
9.3 分组和排序
group by 子句的结果可能是有序,也可能是无序。order by 子句用于对查询的结果进行排序,当语句中有 group by 子句时,order by 用于对分组的结果进行排序。因此,为了保证结果的唯一性,需要在 group by 子句的后面,加上 order by 子句。
9.4 select 子句顺序
下表以在 select 语句中使用时遵循的次序,列出迄今为止所学过的子句。
子句 | 说明 | 是否必须使用 |
---|---|---|
select | 要返回的列或表达式 | 是 |
from | 从中检索数据的表 | 仅在从表中选择数据时使用 |
where | 行级过滤 | 否 |
group by | 分组说明 | 仅在按组 计算聚集时使用 |
having | 组级过滤 | 否 |
order by | 输出排序顺序 | 否 |
10. 使用子查询
子查询又称内部查询是嵌套在其他查询中的查询。而包含子查询的查询称为主查询,又称外部查询。所有的子查询可以分为两类:即相关子查询和非相关子查询:
- 非相关子查询独立于外部查询,子查询总共执行一次,执行完毕后将值传递给外部查询。
- 相关子查询的执行依赖于外部查询的数据,外部查询执行一行,子查询就执行一次。
10.2 利用子查询进行过滤
例如:订单存储在两个表中。Orders 表存储订单编号、客户 ID、订单日期。各订单的物品存储在相关的 orderItems 表中。现在,需要列出订购物品 RGAN01 的所有顾客。
思路:
- 检索包含物品 RGAN01 的所有订单的编号。
- 检索具有前一步列出的订单编号的所有顾客的 ID。
此时,需要将第一步作为子查询,第二步作为主查询。
输入:
select cust_id from orders where order_num in (
select order_num from orderItems where prod_id = "RGAN01");
输出:
+------------+
| cust_id |
+------------+
| 1000000004 |
| 1000000005 |
+------------+
此子查询是非关联子查询,只需要执行一次。
注意:作为子查询的 select 语句只能查询单个列。企图检索多个列将返回错误。
此外,使用子查询并不总是执行这类数据检索的最有效方法。
10.3 作为计算字段使用查询
例如:需要查询 Customers 表中每个顾客的订单总数。订单与相应的顾客 ID 存储在 Orders 表中。
思路:执行这个操作,要遵循下面的步骤:
-
从 Customers 表中检索顾客列表;
-
对于检索出的每个顾客,统计其在 Orders 表中的订单数目。例如,以 cust_id = "100000001"为例,查询该顾客的订单数的语句为:
select count(*) from Orders where cust_id = "100000001";
要对每个顾客执行 count(*),应该将它作为一个子查询。代码如下:
输入:
select cust_name, (select count(*) from Orders where Orders.cust_id = Customers.cust_id) as num from Customers;
输出:
+---------------+------+
| cust_name | num |
+---------------+------+
| Village Toys | 2 |
| Kids Place | 0 |
| Fun4All | 1 |
| Fun4All | 1 |
| The Toy Store | 1 |
+---------------+------+
注:
- 此子查询是关联子查询。对于每条主查询记录,都要执行一遍子查询。
- 由于 Customers 和 Orders 中都含义列名cust_id,所以子查询的 where 子句使用了完全现代列名,以示区分。
11.联结表
11.1 联结
SQL 最强大的功能之一就是能在 select 语句中使用联结( join )表。联结表是 select 语句最重要的功能。
11.1.1 关系表
关系数据库设计的基础是:避免相同的数据出现多次。关系表的设计就是把信息分解成多个表,一类数据一个表。各表之间通过某些共同的值互相关联。例如:有一个产品列表,每种物品为一条记录,存储的信息包括产品描述、价格以及产品供应商。同一供应商会生产多种产品,为此,通常建立一个供应商表进行存储。将供应商信息与产品信息分开存储的理由是:
- 同一供应商生产的每个产品,其供应商信息都是相同的,对每个产品重复此信息是一种浪费。
- 如果供应商信息发生变化,只需要修改供应商表中的某条记录。否则需要修改整个产品表。
- 如果有重复数据(同一供应商有多个产品),很难保证每次输入供应商信息的方式相同,可能导致相同供应商的描述信息不一致。
11.1.2 创建联结
当使用关系表分解信息时,为了从多个表中检索信息,需要使用联结。联结是一种机制,可以从多个表返回一组输出。
创建联结只需要在 select 语句的 from 后面指定要联结的所有表,在 where 后面给出联结的方式。例如,列出每个供应商的每个产品的名称和价格:
输入:
select vend_name, prod_name, prod_price from vendors, products where vendors.vend_id = products.vend_id;
输出:
+-----------------+---------------------+------------+
| vend_name | prod_name | prod_price |
+-----------------+---------------------+------------+
| Bears R Us | 8 inch teddy bear | 5.99 |
| Bears R Us | 12 inch teddy bear | 8.99 |
| Bears R Us | 18 inch teddy bear | 11.99 |
| Doll House Inc. | Fish bean bag toy | 3.49 |
| Doll House Inc. | Bird bean bag toy | 3.49 |
| Doll House Inc. | Rabbit bean bag toy | 3.49 |
| Doll House Inc. | Raggedy Ann | 4.99 |
| Fun and Games | King doll | 9.49 |
| Fun and Games | Queen doll | 9.49 |
+-----------------+---------------------+------------+
完全限定列名
在引用的列可能出现歧义时,必须使用完全限定列名(用一个句点分隔表名和列名)。如果引用一个没有用表名限制的具有歧义的列名,大多数DBMS会报错。
联结的规则
在联结两个表时,实际要做的是将第一个表中的记录和第二个表中的记录的笛卡尔积作为结果的每一行。并用 where 子句过滤不满足条件的结果。如果没有 where 子句,那么select的结果就是两个表的笛卡尔积。
11.1.3 内联结
目前为止使用的联结称为等值联结,它基于两个表之间的相等测试。这种联结也称为内联结(inner join)。内联结也可以使用如下语法:
输入:
select vend_name, prod_name, prod_price from Vendors inner join Products on Vendors.vend_id = Products.vend_id;
分析
此语句和之前语句的结果相同,区别在于:from 关键字后面两个表之间的关系用 inner join指定,且联结条件用on指定,而不是 where。
ANSI SQL规范首选 inner join 语法。具体使用那种语法都可以。
11.1.4 联结多个表
SQL 不限制一条 select 语句中可以联结的表的数目。创建联结的基本规则也相同。首先列出所有表,然后定义表之间的关系。例如,第10课中查询订购商品 RGAN01 的顾客列表:
输入:
select cust_name,cust_address from orderitems, orders, Customers where orderitems.prod_id = "RGAN01" and orderitems.order_num = orders.order_num and orders.cust_id = Customers.cust_id;
输出:
+---------------+---------------------+
| cust_name | cust_address |
+---------------+---------------------+
| Fun4All | 829 Riverside Drive |
| The Toy Store | 4545 53rd Street |
+---------------+---------------------+
12. 创建高级联结
12.1 使用表别名
SQL 除了可以对列名和计算字段使用别名,还允许给表名起别名,原因是:
- 缩短 SQL 语句;
- 允许在一条 select 语句中多次使用相同的表;
例如:
输入:
select cust_name, cust_contact from Customers as C, Orders as O where C.cust_id = O.cust_id;
注意:Oracle 中没有 as
Oracle 不支持 as 关键字。要在 Oracle 中使用别名,可以不用 as,简单地指定列名即可(因此,应该是 Customers C,而不是 Customers as C)。
12.2 使用不同类型的联结
上一章介绍了内联结(等值联结),下面介绍其他三种联结:自联结、自然联结和外联结。
12.2.1 自联结
自联结指:联结的多个表相同,只是采用了不同的别名。例如:查询与 jim jones 在同一个公司的所有顾客。
方法一:采用子查询
输入:
select cust_id, cust_contact from Customers where cust_name = (select cust_name from Customers where cust_contact = 'jim jones');
方法二:
输入:select A.cust_id, A.cust_contact from Customers as B,Customers as A where B.cust_contact = 'jim jones' and B.cust_name = A.cust_name;
输出:
+------------+--------------------+
| cust_id | cust_contact |
+------------+--------------------+
| 1000000003 | Jim Jones |
| 1000000004 | Denise L. Stephens |
+------------+--------------------+
提示:用自联结而不用子查询
自联结通常作为外部语句,用来替代从相同表中检索数据时使用的子查询。虽然最终的结果相同,但是许多 DBMS 处理联结远比处理子查询快得多。
12.2.2 自然联结
使用联结查询时,应该保证查询结果中各表相同的字段只出现一次。此种联结称为自然联结。系统不完成去重工作,由程序员自己完成,一般通过对一个表使用通配符,而明确指定其他表中需要使用的列。
12.2.3 外联结
许多联结将一个表中的行与另一个表中的行相关联,但有时候需要包含没有关联行的那些行,例如:
- 对每个顾客下的订单进行计数,包括那些至今尚未下订单的顾客。
- 列出所有产品以及订购数量,包括没有人订购的产品。
在上述例子中,联结包含了那些在相关表中没有关联行的行。这种联结称为外联结。查询每个顾客的订单编号的查询语句如下:
输入:
select C.cust_id, O.order_num from Customers as C left Outer join Orders as O on C.cust_id = O.cust_id;
输出:
+------------+-----------+
| cust_id | order_num |
+------------+-----------+
| 1000000001 | 20005 |
| 1000000003 | 20006 |
| 1000000004 | 20007 |
| 1000000005 | 20008 |
| 1000000001 | 20009 |
| 1000000002 | NULL |
+------------+-----------+
分析:本例使用了外联结,外联结和内联结的区别在于 inner join 变成了 left outer join,left表明在 left 左边的字段即使没有关联的行,也可以出现在查询结果中。
12.2.4 全联结
全联结检索两个表中的所有行并关联那些可以关联的行。与左联结或右联结包含一个表的不关联的行不同,全外联结包含两个表的不关联的行。用法如下:
输入:
select Customers.cust_id, Orders.order_num from Orders full outer join Customers on Orders.cust_id = Customers.cust_id;
注意:Access, MariaDB, MySQL, SQLite 不支持 full outer join语法。
12.3 使用带聚集函数的联结
聚集函数不仅可以用于从一个表中汇总数据,而且可以与联结一起使用。例如,查询每个顾客的订单数,包括那些没有订单的顾客:
输入:
select C.cust_id, count(O.cust_id) from Customers as C left outer join Orders as O on C.cust_id = O.cust_id group by C.cust_id;
输出:
+------------+------------------+
| cust_id | count(O.cust_id) |
+------------+------------------+
| 1000000001 | 2 |
| 1000000003 | 1 |
| 1000000004 | 1 |
| 1000000005 | 1 |
| 1000000002 | 0 |
+------------+------------------+
13. 组合查询
mysql
中常用的约束:
-
主键约束
primary key
。被修饰过的字段唯一非空。一张表只能有一个主键,这个主键可以包含多个字段。使用方式有三种:
-
建表的同时,添加约束。格式:
字段名 字段类型 primary key
。只能给一个字段添加主键 -
建表的同时,在约束区域添加约束。约束区域是:所有的字段声明完成之后,就是约束区域。格式:
primary key(字段1,字段2)
。可以给多个字段添加主键。示例如下:create table account( id int, name varchar(20), # 所有字段声明完成后,下面区域就是约束区域。可以添加约束。 primary key(id, name) # 语句后不加逗号 );
-
通过修改表结构,添加约束。可以给多个字段添加主键。格式为:
alter table 表名 add primary key(字段1,字段2...)
注:进行此操作时,要求表中没有字段是
primary key
。
-
-
唯一约束
unique
。被修饰过的字段唯一,对null
不起作用。表示各记录被修饰字段的值不相同。各字段的unique
约束没有关联。3种使用方式相似:-
建表的同时,添加约束。格式:
字段名 字段类型 unique
。 -
建表的同时,在约束区域添加约束。
create table account( id int unique, name varchar(20), unique(name) );
-
通过修改表结构,添加约束。
alter table 表名 add unique(字段1,字段2...)
。
-
-
非空约束
not null
。限定被修饰过的字段值非空。方式:create table 表名{ id int not null, };
-
外键约束
foreign key
。 -
自增约束
auto_increment
,- 要求被修饰的字段类型支持自增,一般为
int
。 - 被修饰的字段必须是一个
key
,一般是primary key
。
- 要求被修饰的字段类型支持自增,一般为
truncate
:清空表,会删除整个表,然后再重建一个新表。
格式为:truncate 表名;
truncate
与 delete from
的区别:
delete
属于DML
语句,truncate
属于DDL
语句。delete
逐条删除,truncate
是删除表,再重建一张空表。delete
删除时,不会清空表中自增字段的全局变量。而truncate
重建空表时,自增变量会初始化。
多表操作
案例2--创建多表,可以描述表与表之间的关系。
需求:
把网上商城里面的实体创建成表,并建立他们之间的关系。
网上商城的实体:
用户 订单 商品 分类
常见的关系是:
- 一对多的关系。例如:用户和订单,分类和商品,
多对多的关系:订单和商品 - 一对一的关系
ER
图(实体关系图)
- 实体用方形表示
- 属性用椭圆形表示。
- 实体间的关系用菱形表示。
一对多关系的处理
在开发中将一对多的关系,一方称为主表或一表,多方称为多表或从表。为了表示一对多的关系,一般会在多表中添加一个字段,字段名自定义(建议命名为主表的名称_id),字段类型一般与主表的主关键字的类型相同,称这个字段为外键。
创建用户表
create table user(
id int primary key auto_increment,
name varchar(20)
);
创建订单表
create table myorder(
id int primary key auto_increment,
totalprice double.
user_id int
);
当删除用户时,和该用户对应的订单将无效。为了保证数据的有效性和完整性,需要在多表的一方添加外键约束。格式为:
alter table 多表名 add foreign key(外键名) references 表名(主键);
添加外键约束后,有如下特点:
- 主表中不能删除从表中已引用的数据。
- 从表中不能添加主表中不存在的数据。
- 从表中外键可以为空。
可理解为:添加约束时,系统默认在主表中增加了一个订单数的字段,记录每个用户的引用计数。
- 每当从表中新增或删除一条记录时,系统都会更新其引用的主表的引用计数字段。
- 当从主表中删除一条记录时,系统先判断引用计数字段的值是否为0,若为0,则执行删除;否则,系统报错。
一对一关系的处理(很少用)
在开发中的处理思路:
- 将两个实体合二为一。
- 在一张表上,将主键设置成外键,并添加外键约束。
多对多关系的处理
如订单与商品的关系,一个订单包含多个商品,多个订单可能含有同一种商品。
对于多对多关系的处理,在开发中一般引入一张中间表,在中间表中存放两张表的主键,一般将这两个主键设置为中间表的联合主键。从而将原来两张表之间的多对多关系,转变为两张表与中间表之间的一对多关系。
为了保持数据的有效性和完整性,需要在多表的一方添加外键约 束。