《SQL必知必会》读书笔记
第1课 了解SQL
1.什么是数据库?
我们一直在使用数据库,但对数据库究竟是什么并不清楚。
数据库其实就是一个以某种有组织的方式存储数据的数据集合,也可以理解为是一个存储有组织的数据的容器(通常是一个文件或一组文件)。
注意:这里要区别数据库与数据库软件
- 数据库软件叫数据库管理系统(DBMS),如Oracle、MySQL等等;
- 数据库是DBMS创建和操纵的容器。
2.什么是表?
你可以先把数据库当成一个文件柜,当你往文件柜放资料时,并不是把他们随便扔进某个抽屉就完事了,而是在文件柜中创建文件,然后把相关资料放入特定的文件中。而这里的文件就对应了数据库中的表
表是一种结构化文件,用来存储一种特定类型的数据或清单,也就是说决不能将顾客的清单(顾客ID、顾客名、顾客地址、所在城市、所在州等等)与订单的清单(订单ID、订单日期、订单顾客的ID等等)存储在同一张数据库表中。
此时应该创建两张表,每个清单对应一张表。
注意:
- 数据库中每张表都有一个名字来标识自己,而且在同一个数据库中表名必须唯一。
3.什么是列、数据类型、行?
表有一个或多个列组成,列也叫(表中的)字段。
每个列都对应一种数据类型,数据类型限定了该列中存储的数据种类。例如,防止了数字字段中录入字符串。
表中的数据时按行存储的,每一条记录保存一行。如果把表看成是一个网格,那么垂直的列为表的列,水平的行为表的行。
4.什么是主键?
主键是表中的一列或一组列(几列),能够唯一标识表中的每一行。
注意:
- 主键值是唯一的,也就是说同一张表中不同行的主键值是不一样的;
- 主键值不能为NULL;
- 主键值不允许被修改;
- 主键值不能重用(如果某行在表中被删除了,那么它的主键值不能赋给以后新的行)
5.什么是SQL?
SQL(Structured Query Language),即结构化查询语言。它是一门专门用来与数据库沟通的语言。
标准SQL由ANSI标准协会管理,也叫ANSI SQL。
注意:
- 关键字:SQL组成部分的保留字,不能用作表或列的名字;
- 多条SQL语句时,需要用分号(;)隔开,不过不管是只有一条SQL还是多条SQL,养成每条SQL语句后加上分号总是没错的;
- SQL语句不区分大小写,建议SQL关键字使用大写,列名表名使用小写;
- SQL语句在执行时会忽略空格,所以对SQL语句编写时,推荐适当地缩进与换行,可以使其更方便阅读和调试。
第2课 检索数据
1.什么是SELECT语句?
SELECT 语句的用途是从一个或多个表中检索(查询)信息。
用SELECT检索数据至少包含两部分,(1).想要检索什么信息(select之后的字段名),(2).从哪张表检索(FROM)。
例如:
-- 检索单个列
SELECT prod_name -- SELECT、FROM为关键字
FROM products;
-- 检索多个列
SELECT prod_id, prod_name, prod_price -- 多个列时,用逗号隔开
FROM products;
-- 检索所有列
SELECT * -- 但为了方便阅读与调试,可以选择列出所有列名
FROM products;
注意:使用通配符 * 的优点与缺点
- 优点:可以用来检索未知名字的列(当你不明确列名时,可以使用它)
- 缺点:除非你确实需要表中的每一列,否则建议不要轻易使用,因为检索表中的每一列会降低检索和应用程序的性能
-- 检索出不同的值(即过滤掉重复的值)
SELECT DISTINCT vend_id
FROM products;
注意:DISTINCT 作用于所有的列,DISTINCT跟在SELECT之后。
例如:
SELECT DISTINCT vend_id ,prod_price;
-- 除非指定的这两列出现重复值的格式一致,否则所有行都会被检索出来
-- 上面那句话的意思是,只会过滤掉当两列各自出现重复并且相对位置一致的行
-- 只显示指定行数的内容
-- 1.Oracle中的写法
SELECT prod_name
FROM products
WHERE ROWNUM <= 5; -- 只返回前五行,若表的总行数不足五行,返回所有行
-- 2.MySQL中写法
SELECT prod_name
FROM products
LIMIT 5; -- 同上理
--LIMIT其他用法
SELECT prod_name
FROM products
LIMIT 5 OFFSET 0; -- 返回从第一行开始(包括第一行)的五行数据,若表的行数不够,则返回从第一行开始的表的所有行
-- 也可以写成
LIMIT 0,5; -- 同上理
2.使用注释
- 行内注释:-- (–后有一个空格),# (#后有一个空格,这种方式比较少被支持)
- 多行注释: /* 注释的内容 */
第3课 排序检索数据
ORDER BY 后跟上一个或多个列,从左到右依据对应列对输出进行排序(先按第一个列排序,排完后按第二个列排序,以此类推)
例如:
SELECT prod_name
FROM products
ORDER BY prod_name, prod_price; -- 多个列用逗号隔开
-- 支持相对位置
SELECT prod_id, prod_name, prod_price
FROM products
ORDER BY 2,3; -- 分别依据 prod_name, prod_price进行排序
-- 支持实际列名与相对位置混搭使用
SELECT prod_id, prod_name, prod_price
FROM products
ORDER BY 2, prod_id; -- 分别依据 prod_name, prod_id 进行排序
-- 指定排序方式
SELECT prod_name
FROM products
ORDER BY prod_name DESC, prod_price ASC;
/*
首先 peod_name 降序排序,
然后 prod_price 升序排序(prod_price后面的 ASC可以省略不写)
*/
注意:
- ORDER BY 必须位于SELECT 子句的最后一行,否则会报错;
- ORDER BY 后面跟的列名可以不是SELECT后面的列名;
- ORDER BY 默认使用升序排序;
- 对一些文本性数据进行排序时,根据数据库设置方式的不同,可能需要考虑字母大小写的问题,如:A 与 a。
第4课 过滤数据
1.使用WHERE 子句
通过WHERE 子句来实现
例如:
SELECT prod_name, prod_price
FROM products
WHERE prod_price = 3.49;
注意:
- SQL过滤与应用过滤:
.数据也可以在应用层过滤,但这种做法极其不妥,优化数据库后确实可以更快速有效的对数据进行过滤,但是会让客户端应用或者开发语言处理数据库的工作,这将会极大的影响应用的性能,并且所创建的应用完全不具备可伸缩性。此外,如果在客户端过滤数据,服务器就不得不通过网络发送多余的数据,这将导致网络带宽的浪费。
2.WHERE 子句操作符
注意:
- 字符串需要用单引号括起来;
- 以上操作符并非所有DBMS都支持,需要参阅具体DBMS文档。
3.范围值检查
使用BETWEEN 操作符
SELECT prod_name, prod_price
FROM products
WHERE prod_price BETWEEN 5 AND 10; --包括 5 和 10
4.空值检查
使用 IS NULL
SELECT prod_name
FROM products
WHERE prod_price IS NULL;
SELECT cust_name
FROM customers
WHERE cust_email != 'sales@villagetoys.com' -- cust_email 值为 NULL 的行也会被过滤掉,因为数据库不知道它是否匹配
注意:
- NULL:无值,不同于0、空字符串、仅仅包含空格的字段;
- 如果你想通过过滤不包含指定值来检索表的所有行,借此希望能够返回含NULL值的行,但事实上这是做不到的。因为未知(NULL)有特殊的含义,数据库不知道它们是否匹配,所以在过滤匹配或过滤不匹配时,不会返回它们的结果。
第5课 高级数据过滤
1.组合WHERE子句
-- 通过 AND 给 WHERE 子句添加过滤条件
SELECT prod_id, prod_name, prod_price
FROM products
WHERE vend_id = 'DLL01' AND prod_price <= 4;-- 检索出同时满足所有条件的行
-- 通过 OR 给 WHERE 子句添加过滤条件
SELECT prod_id, prod_name, prod_price
FROM products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01';-- 检索出满足其中任意一个条件的所有行
注意:
- 操作符:用来联结或改变WHERE子句中的子句的关键字,也称为逻辑操作符,eg:AND、IN等等。
2.求值顺序
WHERE 子句可以包含任意数目的 AND 和 OR 操作符,且允许两者结合使用,以进行复杂、高级的过滤。
SELECT prod_name, prod_price
FROM products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'
AND prod_price >= 10;
/*
SQL 执行该 WHERE 子句时,这样理解过滤条件:
有供应商 BRS01 制造的价格为 10 美元以上的所有产品,
以及供应商 DLL01 制造的所有产品
*/
-- 如果想要过滤条件为价格 10 美元以上,且由供应商 BES01、DLL01生产,则可以使用小括号()
SELECT prod_name, prod_price
FROM products
WHERE (vend_id = 'DLL01' OR vend_id = 'BRS01')
AND prod_price >= 10;
注意:
- 当 AND 与 OR 结合使用时,AND 优先执行,如例1;
- AND 与 OR 结合使用时,尽量使用圆括号来明确过滤条件。
3. IN 操作符
SELECT prod_name, prod_price
FROM products
WHERE vend_id IN ('DLL01' , 'BRS01') -- 类似 OR 的功能
ORDER BY prod_name;
注意:
- IN 操作符一般比一组 OR 操作符执行得更快;
- IN 可以包含其他SELECT 语句,即嵌套子查询。
4. NOT 操作符
NOT 后面可以跟任何条件,唯一的作用就是否定跟在它后面的条件。
-- 用于否定NULL
SELECT prod_name
FROM products
WHERE prod_price IS NOT NULL;
-- 用于否定其他过滤条件
SELECT prod_name
FROM products
WHERE NOT vend_id = 'DLL01'
ORDER BY prod_name;
-- 用于否定 IN
SELECT prod_name, prod_price
FROM products
WHERE NOT vend_id IN ('DLL01' , 'BRS01') -- 找出与条件列表不匹配的行
ORDER BY prod_name;
第6课 使用通配符进行过滤
使用LIKE操作符
SELECT prod_id, prod_name
FROM products
WHERE prod_name LIKE 'Fish%'; -- 检索出满足Fish开头的所有行
-- 可以使用多个%
SELECT prod_id, prod_name
FROM products
WHERE prod_name LIKE '%bean bag%'; -- 检索出满足包含bean bag的所有行
-- 根据邮件地址的一部分来查找电子邮件时,可以将通配符放在搜索模式中间
SELECT prod_name
FROM products
WHERE prod_name LIKE 'F%y';
-- 使用 _ 通配符
SELECT prod_id, prod_name
FROM products
WHERE prod_name LIKE '__ inch teddy bear';
-- 使用[]方括号通配符
SELECT cust_contact
FROM customers
WHERE cust_contact LIKE '[JM]%'; -- 表示检索以 J 或 M 开头的任意字符串的所有行
-- 使用[]方括号通配符搭配脱字号^
SELECT cust_contact
FROM customers
WHERE cust_contact LIKE '[^JM]%';-- 表示检索不以 J 或 M 开头的任意字符串的所有行
-- 脱字号的作用可以用 NOT 来替换
WHERE NOT cust_contact LIKE '[JM]%';
注意:
- 搜索模式:由字面值或通配符或两者的组合构成的搜索条件;
- 通配符:用来匹配值的一部分的字符,但无法匹配NULL;
- LIKE 是操作符,但当LIKE做谓词时,就不是操作符了,因为谓词不是操作符;
- 通配符搜索只能用于文本字段(即字符串),非文本字段不能使用;
- 不同DBMS的设置不同,使用通配符时,需要注意数据库是否区分大小写;
- % 可以匹配 0、1、多个任意字符;
- _ 可以匹配一个任意字符;
- [ ]方括号指定了一个字符集,他匹配字符集中的任意一个,使用脱字号 ^(也叫前缀字符) ,则表示不匹配字符集中的任意一个
- 一个空格也占一个通配符;
- 三种通配符可以混搭使用;
- 因为使用通配符检索所需的时间比其他检索更长,所以,如果其他操作符可以达到相同目的,尽量使用其他操作符;
- 如果要使用通配符,尽量不要把通配符放在开始处,因为放在越前面检索就越慢;
第7课 创建计算字段
1.计算机字段
我们需要从数据库中检索出转换、计算或格式化过的数据,而不是检索出数据然后再在客户端应用程序重新格式化。
而使用计算机字段可以达到我们的需求。
注意:
- 计算字段并不是实际存在与表中的,而是运行时在SELECT语句内创建的;
- 只有数据库知道表中那些列是计算机字段,那些列是实际的表列。
2.拼接字段
大体有两种拼接操作符:+ 或者 ||。
例如:
-- Oracle使用 ||
SELECT vend_name || '(' || vend_country || ')'
FROM vendors;
/*许多数据库(不是所有)保存填充为列宽的文本值
比如某列的数据类型为 VARCHAR(50),若该列某行值为'bean',
那么剩余的46位用空格填充
(当然,如果数据类型为 CHAR,则不会用空格填充剩余的长度)
*/
-- 使用TRIM 函数去除填充的空格
SELECT RTRIM(vend_name) || '(' || RTRIM(vend_country) || ')'
FROM vendors;
-- MySQL使用特殊函数 CONCAT
/*
对于数据库保存填充列宽的文本值,
我使用的SQLyog貌似已经帮我们格式化好了,所以不需要用 TRIM 函数
来帮忙消除空格
*/
SELECT CONCAT(vend_name, '(', vend_country, ')')
FROM vendors;
3.别名
因为计算字段实际上没有名字,它只是一个值。而未命名的列不能用于客户端应用中,因为客户端没办法引用它,这是可以通过起别名来解决。
例如:
-- 使用 AS
SELECT RTRIM(vend_name) || '(' || RTRIM(vend_country) || ')'
AS vend_title
FROM vendors;
注意:
- AS 关键字在一些DBMS中是可以省略不写的,但建议写上去保证不出错;
- 别名有时也称导出列;
4.执行算术计算
例如:
SELECT prod_id,
quantity,
item_price,
quantity * item_price AS expanded_price
FROM orderitems
WHERE order_num = 20008
-- 使用SELECT 简单地访问和处理表达式
SELECT 3 * 2;
SELECT TRIM(' abc ');
注意:
- 若列中某行值为NULL,此时与其他列的对应行的算术计算的最后结果一定为NULL;
- SELECT 除了可以用来访问表,还可以用来简单地访问和处理表达式。
第8课 使用函数处理数据
1.函数
函数一般是在数据上执行的,为数据的转换和处理提供方便。
每个DBMS 都有自己特定的函数,只有少数几个函数被所有主要DBMS 等同地支持。
注意:
- 可移植:所编写的代码可以在多个系统上运行;
- 与SQL 语句不一样,SQL函数不是可移植的。
2.使用函数
主要的函数分类(4类):
- 用于处理文本字符串的文本函数;
- 用于处理日期和时间并从这些值中提取特定成分的日期-时间函数;
- 用于数值数据上计算的数值处理函数;
- 返回DBMS 正使用的特殊信息的系统函数。
1.文本函数
SELECT vend_name, UPPER(vend_name) AS vend_name_upcase -- 将文本转换为大写
FROM vendors;
-- 使用 SOUNDEX()
-- customers表中有一个顾客 Kids Place,他的联系名为 Michelle Green
SELECT cust_name, cust_contact
FROM customers
WHERE cust_contact = 'Michael Green'; -- 最终无法获取该用户,因为值不匹配
WHERE SOUNDEX(cust_contact) = SOUNDEX('Michael Green');
/*
使用 SOUNDEX() 函数进行搜索,它匹配所有与'Michael Green'发音类似的联系名,
最后找到了 Michelle Green
*/
注意:
- 函数不仅仅可以用在SELECT 之后,还可以用用WHERE 子句,其他的SQL 语句等等;
- SOUNDEX() :他会匹配和其参数发音类似的值,忽略他们的值是否相同。.
2.日期-时间函数
日期和时间值以特殊的格式存储,以便能够快速和有效地排序或过滤,并且节省物理存储空间。
应用程序一般不使用日期和时间的存储格式,因此日期和时间函数总是用来读取、统计和处理这些值。也因此,日期和时间函数在SQL 中具有很重要的作用,但是它们的可移植性很差(即各个 DBMS 的日期-时间函数很不一致)。
-- Oracle的日期处理函数
SELECT order_num
FROM orders
WHERE to_number(to_char(order_date,'YYYY')) = 2012;
-- to_char: 用来提取日期成分
-- to_number: 将提取的成分转化为数值
-- MySQL 用 YEAR() 来从日期中提取年份
SELECT order_num
FROM orders
WHERE YEAR(order_date) = 2012;
3.数值处理函数
数值处理函数仅仅用于处理数值数据。
在主要的DBMS 中,数值处理函数是最一致的、最统一的函数,但是它相较字符串和日期-时间函数并不常用。
注意:
- 虽然这些函数在格式化、处理和过滤数据中非常有用,但它们在各种SQL 实现中很不一致。
第9课 汇总数据
1.聚集函数
聚集函数就是对一个或多个行进行计算并返回一个值的函数。
-- AVG() 函数:可以用来返回某列的平均值或者某列上特定几行的平均值
-- 1.返回某列的平均值
SELECT AVG(prod_price) AS avg_price
FROM products;
-- 2.返回某列上特定几行的平均值
SELECT AVG(prod_price) AS avg_price
FROM products;
WHERE vend_id = 'DLL01';
注意:
- AVG()函数只能作用于单列,要获得多个列的平均值,可以通过使用多个AVG()函数做到;
- AVG()函数只能用来获取数值类型的列的平均值,而且列名要作为参数传入AVG()函数;
- AVG()函数会忽略值为NULL的行。
-- COUNT() 函数:用于计算表中行的数目或者指定行的数目
-- 1.COUNT()函数的两种用法
-- 1.1 使用COUNT(*) 包含表列中存在NULL值的行也会纳入计算
-- 1.2 使用COUNT(column) 表列中值为NULL的行不纳入计算
-- 2.计算表中行的数目
-- 2.1 使用COUNT(*)
SELECT COUNT(*) AS num_cust
FROM customers;
-- 2.2 使用COUNT(column)
SELECT COUNT(cust_email) AS num_cust
FROM customers;
-- 3.计算指定行的数目
-- 3.1 NULL值不纳入计算
SELECT COUNT(cust_email) AS num_cust
FROM customers
WHERE cust_id = '1000000005';
-- 3.1 忽略NULL值,纳入计算
SELECT COUNT(*) AS num_cust
FROM customers
WHERE cust_id = '1000000005';
-- MAX()函数:返回指定列的最大值,需要指定列名
SELECT MAX(prod_price) AS max_price
FROM products;
注意:
- MAX()函数可以用来返回任意列中的最大值,包括文本列中的最大值,用于文本数据时,返回的是该列排序后的最后一行;
- 当然, MAX()函数一般用于返回数值或日期的最大值;
- MAX()函数会忽略NULL值;
- MIN()函数的用法和注意事项和MAX()函数差不多,但功能相反。
-- SUM()函数用于返回指定列的所有行值的总和或者指定几行值的总和
-- 1.1 返回指定列的所有行值的总和
SELECT SUM(quantity) AS items_ordered
FROM orderItems;
-- 1.2 返回指定列的指定几行值的总和
SELECT SUM(quantity) AS items_ordered
FROM orderItems
FROM order_num = 20005;
-- 2. SUM()函数还可以用来合计计算
SELECT SUM(item_price * quantity) AS total_price
FROM orderItems
FROM order_num = 20005;
/*
先计算(每一行)每项物品的总价格,再合计所有行总的金额
*/
注意:
- SUM()函数忽略NULL值的行。
2.聚集不同值
指定参数ALL或者不指定参数(ALL是默认行为),对所有行执行计算;
指定参数DISTINCT,只对包含不同值的行执行计算。
SELECT AVG(DISTINCT prod_price) AS avg_price
FROM products
WHERE vend_id = 'DLL01';
注意:
- DISTINCT 不能用于COUNT(*),只能用于COUNT(DISTINCT column);
- DISTINCT 后面只能跟列名,不能跟计算或者表达式;
- MAX()、MIN()使用DISSTINCT没有意义;
- 除了DISTINCT、ALL,DBMS 还支持其他的参数,比如支持对查询结果的子集进行计算的TOP和TOP PERCENT等等。
3.组合聚合函数
SELECT COUNT(*) AS num_items,
MIN(prod_price) AS min_price,
MAX(prod_price) AS max_price,
AVG(prod_price) AS avg_price
FROM products;
总结:使用聚集函数我们可以对行进行计数、计算和、计算平均数,不用检索所有数据就可以获得最大值、最小值。
第10课 分组数据
1.分组
分组涉及两个SELECT语句子句:GROUP BY 子句和 HAVING 子句。
分组做了三件事:(1).将数据分为多个逻辑组;(2). 会自动对分组的列进行升序排序;
(3). 然后对表的每一个列按分组后每组所指定的那几行进行聚集计算(包括计算总行数、平均值等等)(或者可以理解为对每个组进行聚集计算)。
SELECT vend_id, COUNT(*) AS num_prod
FROM products
GROUP BY vend_id;
-- 使用相对位置进行分组
SELECT vend_id, prod_price
FROM products
GROUP BY 1, 2; -- 如果过两个列的格式不一致则返回所有行
注意:
- GROUP BY 后面可以跟任意数目的列;
- SELECT 后面跟的列只能是来自GROUP BY 后面的列,SELECT 后面还可以跟聚合函数,聚合函数的参数可以不是GROUP BY 后面的列;
- GROUP BY 后面可以跟列、表达式,但不能跟聚合函数,也不能使用别名;
- 如果分组的列存在一个或多个NULL值,那么这些NULL值归为一组;
- GROUP BY 必须在WHERE 子句之后,ORDER BY 子句之前。
2.过滤分组
HAVING 用于分组的过滤,WHERE 用于行的过滤,两者的用法相同,只是目的不同。
SELECT cust_id, COUNT(*) AS os
FROM orders
GROUP BY cust_id
HAVING COUNT(*) >= 2;
注意:
- WHERE 是在分组前执行,HAVING 是在分组后执行,WHERE 过滤掉的行不包含在分组内;
- WHERE 子句和HAVING 子句可以组合使用。
SELECT vend_id, COUNT(*) AS num_prod
FROM products
WHERE prod_price >= 4
GROUP BY vend_id
HAVING COUNT(*) >= 2;
/*
先过滤掉prod_price小于4的行,然后再进行分组,分组后再过滤掉计数小于2的组
*/
3.分组和排序
虽然GROUP BY 会进行排序,但只是针对GROUP BY后面的列进行的排序,并不能按照我们的预期去排序,所以这时需要使用ORDER BY 来按照我们的预期进行排序。
SELECT order_num, COUNT(*) AS items
FROM orderItems
GROUP BY order_num
HAVING COUNT(*) >= 3
ORDER BY items, order_num;
/*
4.SELECT 子句的顺序
第11课 使用子查询
1.子查询作为过滤条件
-- 作为过滤条件
SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
FROM orderItems
WHERE prod_id = 'RGAN01');
-- 嵌套多层子查询
SELECT cust_name, cust_contact
FROM custmoers
WHERE cust_id IN (SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
FROM orderItems
WHERE prod_id = 'RGAN01'));
注意:
- 子查询总是由内向外处理;
- 可以对子查询进行适当缩进和换行,以便方便阅读和调试;
- 虽然子查询嵌套的数目没有规定,但是不建议嵌套过多层子查询,会影响检索性能;
- 作为子查询的SELECT语句只能查询单个列,查询结果可以是单个值,也可以是集合,查询多列会报错。
2.子查询作为计算字段
SELECT cust_name,
cust_state,
(SELECT COUNT(*)
FROM orders
WHERE orders.cust_id = customers.cust_id) AS os
FROM customers
ORDER BY cust_name;
/*
这里要是没有使用完全面定列名 orders.cust_id,customers.cust_id
而是直接用cust_id,那么 DBMS 会认为是orders表在自己连自己
*/
注意:
- 当SELECT 语句中出现多张表,且存在同名的列时,应该使用完全限定列名来区别。
第12课 联结表
1.联结
关系表的设计就是要把信息分解成多个表,一类数据一个表。各表通过某些共同的值互相关联(所以才叫关系数据库)。
可伸缩:能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称为可伸缩性好。
数据分解为多个表能够更有效地存储、更方便地处理,并且伸缩性更好。
联结:其实就是一种机制,用来在一条SELECT语句中关联表。
2.创建联结
-- 等值联结
SELECT vend_name, prod_name, prod_price
FROM vendors, products
WHERE vendors.vend_id = products.vend_id;
注意:
- 使用联结需要使用完全限定列名;
- 一体SELECT语句在联结几张表时,相应关系是在运行中构造的;
- 查询多张表时没有使用WHERE 子句进行联结,则返回的是笛卡尔积,而返回笛卡尔积的联结也叫叉联结。
3.内联结
等值联结:基于两张表的相等测试,也叫内联结,是最常用的联结。
使用(INNER)JOIN ON。
SELECT vend_name, prod_name, prod_price
FROM vendors INNER JOIN products
ON vendors.cust_id = products.cust_id;
--这种写法相当于上面那种用 WHERE 子句来实现的写法
-- 联结多张表
-- 1.使用 WHERE
SELECT prod_name, vend_name, prod_price, quantity
FROM orderItems os, products p, vendors v
WHERE p.vend_id = v.vend_id
AND os.prod_id = p.prod_id
AND order_num = 20007;
-- 2.使用 JOIN
SELECT prod_name, vend_name, prod_price, quantity
FROM orderItems os
INNER JOIN products p ON os.prod_id = p.prod_id
INNER JOIN vendors v ON p.vend_id = v.vend_id
AND order_num = 20007;
第13课 创建高级联结
1.使用别名
好处:
- 可以简化SQL语句;
- 允许在一条SELECT 语句中多次使用相同的表(如:自联结)。
注意: - 在Oracle中起别名不能加AS,MySQL可以加也可以不加;
- 表别名只是在查询的时候使用,与列别名不同,他不需要返回客户端。
2.使用不同类型的联结
分自联结、自然联结和外联结三种。
1.自联结
-- 需要起别名,方便同一SELECT 语句中多次引用相同的表
SELECT cust_id, cust_name, cust_contact
FROM customers AS c1, customers AS c2
WHERE c1.cust_name = c2.cust_name
AND c2.cust_contact = 'Jim Jones';
-- 使用子查询达到查询到相同的结果
SELECT cust_id, cust_name, cust_contact
FROM customers
WHERE cust_name = (SELECT cust_name
FROM customers
WHERE cust_contact = 'Jim Jones');
注意:
- 如果自联结与子查询能达到相同的效果,优先选择使用自联结,因为大多数DBMS处理自联结比处理子查询快得多。
2.自然联结
所有的内联结(等值联结)都是自然联结。
内联结有一个问题就是会返回所有的数据,所以相同的列可能会多次出现,那么我们可以通过对一个表使用通配符(SELECT *),而对其他表的列明确指明出来,这样子来保证每一列只出现一次。
3.外联结
上面的联结都只会显示有关联的行,但有时候联结两个表后需要包含一些没有关联的行。
而外联结就可以做到。
外联结:就是联结那些相关表中没有关联的行。
分三种外联结:左外联结、右外联结(与左外联结可以相互转换,只要调换一下表的前后位置即可)、全外联结。
-- 左外联结:包含左表所有无关联的行
SELECT c.cust_id, o.order_num
FROM customers c LEFT OUTER JOIN orders o
ON c.cust_id = o.cust_id;
-- 全外联结:包含左、右两张表中所有无关联的行。MySQL不支持全外联结
SELECT c.cust_id, o.order_num
FROM customers c FULL OUTER JOIN orders o
ON c.cust_id = o.cust_id;
-- 使用带聚集函数的联结
SELECT c.cust_id, COUNT(o.order_num) AS num_ord
FROM customers c FULL OUTER JOIN orders o
ON c.cust_id = o.cust_id
GROUP BY c.cust_id;
注意:
- 一个联结可以包含多张表,甚至可以对不同的表使用不同的联结类型,但是最好在有多个联结的情况下,先一个个测试他们是否正确,最后得出最终式。
第14课 组合查询
SQL 一般只有一条SELECT 语句,返回单个查询结果,但也可以有多条SELECT语句,通过UNION关联起来,返回查询结果集。
有时候多个SELECT 语句的查询结果中可能会有重复的行,若使用UNION 关联这些SELECT 语句,那么重复的行会被自动过滤掉;若你想要返回所有的行,包括重复的行,可以使用UNION ALL。
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI') -- 这里没有分号(;)结尾!
UNION
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = 'Fun4All'
ORDER BY cust_name,cust_contact;
-- 相当于执行多条SELECT语句
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI')
AND cust_name = 'Fun4All'
ORDER BY cust_name,cust_contact;
注意:
- 其实UNION 的作用相当于执行多条SELECT语句,理论上讲,他们之间没有什么实际差别;
- UNION 需要由两条及以上的SELECT 语句组成;
- UNION 关联的每条SELECT语句,SELECT 后面跟的列要一致,但列的顺序可以不一样;
- 每条SELECT 语句对应的列的数据类型要兼容(即数据类型要一样或者可以隐式转换的类型(例如:不同的数值类型、不同的日期类型));
- ORDER BY 必须位于最后一条SELECT 语句的最后一行;
- 还有其他类型的UNION ,EXCEPT(有时也称 MINUS)可用来检索第一个表中存在而第二个表中不存在的行;INTERSECT,可以用来检索两个表中都存在的行;但实际上,这两种类型很少使用,因为这些结果可以通过联结做到;
- 实际上,UNION 除了可以组合同一张表中的多个查询,还可以用来组合多个表的数据,即使是有不匹配列名的表,在这种情况下,可以将UNION 与别名组合,检索一个结果集。
第15课 插入数据
有三种方式
-- 插入所有行
-- 1.不指定插入的对应字段,这个非常依赖于表列的次序,如果表的结构发生变化,可能会导致插入失败,很不安全
INSERT INTO customers
VALUES('1000000006',
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111',
'USA',
NULL,
NULL);
-- 2.指定插入的字段,VALUES里面的值与指定的列对应,指定的列的顺序可以随便放,比较灵活,即使表的结构发生变化也不会影响插入,更安全
INSERT INTO customers(cust_id,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country,
cust_contact,
cust_email)
VALUES('1000000006',
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111',
'USA',
NULL,
NULL);
-- 插入部分行
/* 前提是表中的某些列是允许NULL值(约束为 NOT NULL)或者设置有默认值的,
比如cust_contact、cust_email是允许NULL,
或者有默认值的,此时不指定他们,也不给他们赋值,
那么他们就使用NULL 值或者默认值 */
INSERT INTO customers(cust_id,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country)
VALUES('1000000006',
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111',
'USA');
-- 插入检索出的数据
-- 1.使用INSERT SELECT 将检索出的数据插入到表中
INSERT INTO customers(cust_id,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country)
SELECT cust_id, -- SELECT 这里的列名可以不用与上面的字段匹配,但数据类型要匹配
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country
FROM custNew;
-- 2.从一个表复制到另一个表
-- 这条SELECT 语句创建了一个名为custCopy的新表,并把customers表中的数据全部复制到custCopy
SELECT *
INTO custCopy
FROM customers;
-- Oracle、MySQL中的写法
CREATE TABLE custCopy AS
SELECT * FROM customers;
注意:
- 对于一些DBMS,INSERT INTO 的INTO 可以省略不写,但考虑可移植性,建议写;
- INSERT 语句只插入一行,如果要插入多行,需要 执行多个INSERT 语句;
- INSERT SELECT 是导出数据,而SELECT INTO 是导入数据;
第16课 更新和删除数据
1.更新数据
-- 1.更新所有行
UPDATE customers
SET cust_email = 'kim@thetoystore.com';
-- 2.更新部分数据,使用 WHERE 子句
UPDATE customers
SET cust_contact = 'Sam Roberts',
cust_email = 'sam@toyland.com' -- 多个列用逗号隔开(,)
WHERE cust_id = '1000000005';
-- 3.删除某个列的值,可设置它为 NULL
UPDATE customers
SET cust_email = NULL
WHERE cust_id = '1000000005';
注意:
- UPDATE 语句中可以使用子查询;
- 有的DBMS 支持使用FROM 关键字,表示用一个表的数据更新另一个表的行;
2.删除数据
-- 1.删除特定行,使用 WHERE子句
DELETE FROM customers
WHERE cust_id = '1000000006';
-- 2.删除所有行,它是把表的每一行逐个删除
-- 2.1 使用DELETE
DELETE FROM customers;
-- 2.2 一种更快的删除,它会先把整张表删除,然后再创建一张一模一样的表(此时表中没有任何数据)
TRUNCATE TABLE customers
注意:
- 使用外键确保引用完整性的一个好处就是,DBMS 通常可以防止删除某个关系需要用到的行,比如:要从products表中删除一个产品,而这个产品用在orderItems表的订单中,那么DELETE 语句将会抛出错误并中止;
- 引用完整性表示DBMS强制实施数据完整性规则,这些规则一般由提供了界面的DBMS 管理。
第17课 创建和操纵表
1.创建表
使用 CREATE TABLE
-- 创建表
CREATE TABLE products ( -- 创建表时指定表名因该是不存在的
prod_id CHAR(10) NOT NULL, -- 各个列之间用逗号隔开,最后一列不用加逗号
vend_id CHAR(10) NOT NULL,
prod_name CHAR(254) NOT NULL DEFAULT 'Dale', -- DEFAULT 关键字用于指定默认值
prod_price DECIMAL(8,2) NOT NULL, -- DECIMAL表示 高精度小数类型,8 表示一共有八位数字,2 表示小数点后保留两位
prod_desc VARCHAR(1000) NULL -- 在MySQL中VARCHAR(1000) 必须替换为 TEXT
);
注意:
- 创建表时,若非空约束为NULL,那么该列允许在插入或者更新时不给值,若为NOT NULL 关键字,那么该列在插入或更新时必须给值(若设有默认值,则可以不给值),否则会报错;
- DEFAULT 关键字常用于日期或时间戳 列;
- 指定日期:MySQL用户指定DEFAULT CURRENT_DATE()、Oracle用户指定DEFAULT SYSDATE ;
- 尽量使用默认值,少用NULL关键字。(这只是个习惯,并不是说那个更优)
2.更新表
使用 ALTER TABLE
-- 给已知表添加列
ALTER TABLE vendors
ADD vend_phone CHAR(20);
-- 删除列
ALTER TABLE vendors
DROP COLUMN vend_phone;
3.删除表
使用 DROP TABLE
DROP TABLE custCopy;
4.重命名
不同的DBMS SQL实现不同,MySQL、Oracle 使用RENAME语句;
重命名操作需要指定旧表名和新表名。
总结:
ALTER、DROP 这些语句应该小心使用,并且应该在备份之后使用。
第18课 使用视图
1.视图
- 视图是一张虚拟的表,它没有存储任何数据,仅包含了一条查询;
- 使用视图的好处:(1). 对SQL语句进行了封装,可以重复使用它;
(2). 使用的只是表的一部分数据(视图包含一条查询,查询的结果集只是表的一部分数据),所以我们可以通过赋予用户访问视图的权限,而不是赋予用户访问整张表的权限,从而很好地保护了数据;
(3). 更改数据格式和表示(创建视图之后,可以对视图进行过滤、分组、与其他视图或表联结、更新、添加数据等等;(其实就是对视图里的SELECT 语句进行我们之前学过的任何操作,许多DBMS禁止在视图使用ORDER BY)) - 视图仅仅是用来查看存储在别处数据的一种设施;
- 如果用多个联结和过滤创建了复杂的视图或者嵌套了视图,性能可能会下降的很厉害。
2.视图的规则和限制
- 和表一样,视图名唯一(不能给视图取和别的视图或者和表一样的名字);
- 创建视图必须有足够访问权限,这个一般由DBA赋予;
- 视图可以嵌套(根据从其他视图中检索出来的数据来创建视图),类似子查询;
- 许多DBMS禁止在视图中使用ORDER BY (排序);
- 如果SELECT 后面有计算字段,需要起别名;
- 视图不能索引,也不能有相关触发器和默认值;
- 有些DBMS 把视图作为只读的查询,不能向底层写回数据;
3.创建视图
-- 创建视图
CREATE VIEW productCustomers AS
SELECT cust_name, cust_contact, prod_id
FROM customers c,orders o, orderItems oi
WHERE c.cust_id = o.cust_id
AND oi.order_num = o.order_num;
-- 从视图中查询数据
SELECT cust_name, cust_contact
FROM productCustomers
WHERE prod_id = 'RGAN01';
-- 重新格式化检索出来的数据
CREATE VIEW vendorLocations AS
SELECT RTRIM(vend_name) || '(' || RTRIM(vend_country) || ')' AS vend_title
FROM vendors;
-- 使用视图简化计算字段,使用视图可以极大地简化数据处理
CREATE VIEW orderItemsExpanded AS
SELECT order_num,
prod_id,
quantity,
item_price,
quantity * item_price AS expanded_price
FROM orderItems;
--使用
SELECT *
FROM orderItemsExpanded
WHERE order_num = 20008;
总结:视图用来封装SQL语句,简化数据处理,重新格式化基础数据,保护基础数据等等。
第19课 使用存储过程
1.什么是存储过程
存储过程:封装了SQL语句,需要的时候就可以调用,类似函数;换句话说,存储过程就是为以后使用而保存的一条或多条SQL语句。
2.为什么使用存储过程额(简单、安全、高性能)
- 通过把用于处理的SQL语句封装在一个易用单元里,简化复杂的操作;
- 不用再去建立一系列处理步骤,保证了数据的一致性,例如:所有开发人员和应用程序都使用同一存储过程,则所使用的的代码都是一样的,那么结果肯定也是相同的。这一点防止了因为过多的操作步骤可能导致出错的问题;
- 简化对变动的管理,如果表名,列名或业务逻辑发生了变化,那么我们只需要更改存储过程的代码,使用它的人员不需要知道这些变化,这一点延伸了安全性;
- 因为存储过程通常以预编译的形式存储,DBMS 所需的工作量很少,提高了性能等等。
3.创建、执行存储过程
-- Oracle版本
-- 1.创建存储过程
CREATE PROCEDURE mailingListCount(listCount out INTEGER) -- 三个参数 IN 传递值给存储过程(其实是给存储过程传入实参),OUT 从存储过程返回值给变量,INTOUT 给存储过程传参同时从存储过程返回值给变量
AS -- 这里也可以使用 IS
v_rows INTEGER; --声明变量及类型
BEGIN
SELECT COUNT(*) INTO v_rows
FROM customers
WHERE NOT cust_email IS NULL; -- SELECT 语句结束,加分号
listCount := v_rows; -- 加分号
END; -- 加分号
-- 2.执行存储过程
var returnValue NUMBER
EXEC mailingListCount(:returnValue); -- 也可以使用CALL 代替 EXEC
SELECT returnValue;
第20课 管理事务处理
1.事务处理
- 使用事务处理,通过确保成批的SQL操作要么全部执行成功,要么全部执行失败,来维护数据库的完整性。
- 具体地讲:事务处理是一种机制,用来管理成批执行的SQL操作,保证数据库不会包含不完整的操作结果。利用事务处理,可以保证一组操作不会中途停止,他们要么全部执行成功,要么全部执行失败。如果没有错误发生,整组语句的执行结果会提交到数据库;如果发生错误,则进行回退,将数据库恢复到某个已知且安全的状态。
- 关于事务处理的四个术语:
(1).事务:一组SQL语句;
(2).回退:发生错误时,撤销相应SQL操作的过程;
(3).提交:没有发生错误,将为存储的SQL操作结果写入数据库;
(4).保留点:事务处理中设置的临时占位符,可以指定它发布回退。 - 事务处理用来管理 INSERT 、UPDATE 、DELETE 语句,对 SELECT 、CREATE 、DROP 进行回退没有效果。
2.控制事务处理
-- 不同 DBMS 开启事务处理的方式不同
-- MySQL版本,提交事务
START TRANSACTION -- 手动开启事务
DELETE orderItems WHERE order_num = 12345;
DELETE orders WHERE order_num = 12345;
COMMIT;
-- Oracle版本,提交事务
SET TRANSACTION -- 手动开启事务
DELETE orderItems WHERE order_num = 12345;
DELETE orders WHERE order_num = 12345;
COMMIT;
-- MySQL版本,回滚事务
START TRANSACTION -- 手动开启事务
DELETE orderItems WHERE order_num = 12345;
DELETE orders WHERE order_num = 12345;
ROLLBACK; -- 没有指定保留点,则回滚到开启事务的位置 START TRANSACTION
-- Oracle版本,回滚事务
SET TRANSACTION -- 手动开启事务
DELETE orderItems WHERE order_num = 12345;
DELETE orders WHERE order_num = 12345;
ROLLBACK; -- 同上理
-- MySQL版本,回滚事务,使用保留点
START TRANSACTION -- 手动开启事务
INSERT INTO customers(cust_id, cust_name)
VALUES('1000000010', 'Toys Emporium');
SAVEPOINT deltel1; -- 设置保留点,一个事务中可以设置多个保留点
DELETE orderItems WHERE order_num = 12345;
SAVEPOINT deltel2; -- 设置保留点
DELETE orders WHERE order_num = 12345;
ROLLBACK TO deltel1; -- 回滚到指定保留点的位置 SAVEPOINT deltel1;
-- Oracle版本,回滚事务,使用保留点
SET TRANSACTION -- 手动开启事务
INSERT INTO customers(cust_id, cust_name)
VALUES('1000000010', 'Toys Emporium');
SAVEPOINT deltel1; -- 设置保留点,一个事务中可以设置多个保留点
DELETE orderItems WHERE order_num = 12345;
SAVEPOINT deltel2; -- 设置保留点
DELETE orders WHERE order_num = 12345;
ROLLBACK TO deltel1; -- 同上理
第21课 使用游标
1.游标
- 游标的用途:有时,需要在检索出来的行中前进 、后退一行或几行。
- 游标是一个存储在 DBMS 服务器上的数据库查询,它不是一条 SELECT 语句,而是被该SELECT 语句检索出来 的结果集。在存储了游标之后,数据库可以根据需要滚动或浏览其中的数据。(声明了游标之后,下次使用它就不需要声明了,只需要打开游标,然后取出数据等等)
- 可以设置游标的一些特性:
(1).能够标记游标为只读,使数据能够读取,但不能更新 、删除;
(2).能够控制可以执行的定向操作(向前 、向后 、第一 、最后 、绝对位置 、相对位置);
(3).能标记某些列为可编辑,某些列为不可编辑;
(4).规定范围,使游标对创建它的特定请求(如存储过程)或所有请求可访问; - 游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。
2.使用游标
使用游标的过程共有四步:
- 声明游标(此时还没有从数据库中检索数据,它只是定义了要使用的 SELECT 语句和游标选项);
- 打开游标后才可以使用,这可时候就会执行之前定义的 SELECT 语句把数据检索出来;
- 根据需要取出检索出来的数据的一行或多行;
- 使用完后,记得关闭游标。
注意:可以根据需要在打开游标之后,关闭游标之前,频繁地进行取操作(使用FETCH)。
-- MySQL版本,创建游标 、使用游标 、关闭游标
delimiter //
create procedure myprocedure(out ret int)
begin
declare done boolean default 0;
declare mycursor cursor for -- 声明游标
select col1 from mytable;
# 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
declare continue handler for sqlstate '02000' set done = 1;
open mycursor; -- 打开游标
repeat
fetch mycursor into ret; -- 通过fetch 把游标里的数据放到变量ret中
select ret;
until done end repeat;
close mycursor; -- 关闭游标
end //
delimiter ;
-- Oracle版本,创建游标 、使用游标 、关闭游标
DECLARE TYPE custCursor IS REF CURSOR
RETURN customers%ROWTYPE;
DECLARE custRecord customer%ROWTYPE
BEGIN
OPEN custCursor;
FETCH custCursor INTO custRecord;
CLOSE custCursor;
END;
第22课 高级SQL特性
1.约束
1.主键约束:
(1).任意两行主键值不同;
(2).每行都具有一个主键值(不允许为NULL值);
(3).主键值的列从不修改 、更新;
(4).主键值不能复用。
-- 建表声明
CREATE TABLE vendors(
vend_id CHAR(10) NOT NULL PRIMARY KEY,
···
);
-- 使用CONSTRAINT语法声明
ALTER TABLE vendors
ADD CONSTRAINT PRIMARY KEY (vend_id);
2.外键约束:外键是表中的一列,该列及其值对应另一张表的主键及主键值,外键是保证引用完整性的重要部分。
引用完整性:关系数据库存储分解为多张表的数据,每个表存储相应的数据。利用键来建立从一个表到另一个表的引用。
-- 创建表来声明外键
CREATE TABLE orders(
···
cust_id CHAR(10) NOT NULL REFERENCES customers(cust_id)
);
-- 使用 CONSTRAINT 语法
ALTER TABLE orders
ADD CONSTRAINT
FOREIGN KEY (cust_id) REFERENCES customers(cust_id);
注意:
(1).在定义了外键之后,DBMS 不允许删除该外键在另一个表中相关联的行,需要先删除该外键指定的行,才能删除另一张表中对应的行;
(2).级联删除:该特性是若删除了表中的某行,那么其他关联该行的数据都会被删除(其他表中相关数据都会被删除)。
3. 唯一约束:用来保证一列(一组列)中数据是唯一的
(1).一张表可以有多个列唯一约束,但主键只能有一个;
(2).唯一约束的列可以包含多个NULL值;
(3).唯一约束列可以修改 、更新;
(4).唯一约束列的值可以重复使用;
(5).与主键不同,唯一约束列不能用来定义外键;
(6).建表或者使用CONSTRAINT语法时,使用UNIQUE 实现唯一约束。
4.检查约束:数据类型限定了可以保存在列中的数据的类型,检查约束在数据类型内做了进一步的限定
(1).检查最大值 、最小值;
(2).指定范围;
(3).只允许特定值(如:性别字段只能出现 F 或 M)。
-- 建表声明
CREATE TABLE orderItems(
···
quantity INTEGER NOT NULL CHECK(quantity > 0),
···
);
-- 使用CONSTRAINT
ALTER TABLE orderItems
ADD CONSTRAINT CHECK(gender LIKE '[FM]');
2.索引
1.索引用来排序数据以加快搜索和排序操作的速度;
2. 可以在一个或多个列上定义索引;
3. 索引改善了检索操作的性能,但降低了数据插入 、更新 、删除的性能,因为在执行增删改后,需要DBMS 动态更新索引;
4. 索引数据可能要占用大量的存储空间;
5. 并非所有数据都适合做索引,比如取值不多的数据,州等等这些;
6. 索引用于数据过滤和数据排序,如果你经常以某种特定的顺序排序数据,那么该数据可能适合做索引;
7. 拓展第二点,可以在索引中定义多个列(如,州加上城市),这样的索引仅在州加上城市的顺序排序时有用,即如果想要按城市排序,这种索引没有用;
8. 索引必须唯一命名。
--创建索引
CREATE INDEX prod_name_ind
ON products(prod_name); -- 圆括号内可以使多列,用逗号隔开
3.触发器
1.触发器是特殊的存储过程,只有在 INSERT 、UPDATE 、DELETE操作的会后触发;
2.与存储过程不同的是,存储过程只是单纯地存储SQL语句,而触发器与单个表关联,如orders上的DELETE操作关联的触发器,只会在orders表发生删除时触发。
3.触发器常见用途:
(1).保证数据一致,如UPDATE 或 DELETE 操作中将所有州名转换为大写;
(2).根据某个表的变动而对其他表执行相关操作,如每当更新 、删除一行时,将审计跟踪记录写入某个日志表中;
(3).对数据进行额外的验证根据需要回退数据,如保证某个客户的可用资金不超过限定,如果超过,则阻塞插入;
(4).计算机算列的值或更新时间戳。
4.约束的处理速度比触发器块,所以如果可以的话,尽量使用约束代替触发器。
-- Oracle实现触发器
CREATE TRIGGER customer_state
AFTER INSERT OR UPDATE --指定是BEFORE还是AFTER 来指定是在相关操作之前执行还是之后执行
FOR EACH ROW
BEGIN
UPDATE customers
SET cust_state = UPPER(cust_state)
WHERE customers.cust_id = :OLD.cust _id --DELETE 触发一个OLD的虚拟表,并且是只读的,UPDATE 触发一个NEW和OLD的表,NEW 可被修改,OLD 可读
END;
-- MySQL中不允许触发器中使用CALL,即不能调用存储过程