7. 数据处理函数
前面介绍的去除数据首位空格的 trim()
函数就是数据处理函数,除此之外还有多种其他类型的数据处理函数:
- 用于处理文本串的文本函数,如删除或填充值,转换值为大写或小写。
- 用于在数值数据上进行算术操作的数值函数,如返回绝对值,进行代数运算。
- 用于处理日期和时间值并从这些值中提取特定成分的日期和时间函数,例如,返回两个日期之差,检查日期有效性等。
- 系统函数,如返回用户登录信息,检查版本细节。
在不了解如何使用一个函数的时候,可以使用 help
命令,比如 help substr
就可以获取 substr
的使用方式和示例。
7.1 字符函数
函数 | 说明 |
left() 、right() |
返回串左边、右边的字符 |
length() |
返回串长度 |
lower() 、upper() |
返回串的小写、大写 |
rtrim() 、ltrim() 、trim() |
去除右边、左边、两边的空格 |
locate() |
找出一个串的子串 |
soundex() |
返回串的 sundex 值 |
substring() |
返回子串的字符 |
subset() |
返回子串的字符(和 substring 使用方式不一样) |
instr() |
返回子串第一次出现的索引,没有返回 0 |
replace() |
字符串替换 |
lpad() 、rpad() |
左填充、右填充 |
示例:
# upper、lower 将姓变大写,名变小写,然后拼接 select concat(upper(last_name), lower(first_name)) 姓名 from employees; # 姓名中首字符大写,其他字符小写然后用_拼接,显示出来 select concat(upper(substr(last_name, 1, 1)), '_', lower(substr(last_name, 2))) from employees; # substr 截取字符串,sql 中索引从 1 开始而不是0 select substr('hello world', 3); # llo world select substr('hello world', 2, 3); # ell # instr 返回子串第一次出现的索引,没找到返回 0 select instr('abcdefg', 'cd'); # 3 # trim 减去字符串首尾的空格或指定字符 select trim(' hello '); # hello select trim('aa' from 'aaabaabaaaaaa'); # abaab # lpad 用指定的字符实现左填充指定长度 select lpad('he', 5, '-'); # ---he # rpad 用指定的字符实现左填充指定长度 select rpad('he', 5, '-*'); # he-*- # replace 替换 select replace('abcabcabc', 'bc', '--'); # a--a--a-- 复制代码
7.2 数学函数
函数 | 说明 |
round() |
四舍五入 |
ceil() |
向上取整 |
floor() |
向下取整 |
truncate() |
保留几位小数 |
mod() |
取余 |
abs() |
返回绝对值 |
rand() |
返回一个随机数 |
示例:
# round 四舍五入,第二个参数是小数点后保留的位数 select round(-1.55); # -2 select round(1.446, 2); # 1.45 # ceil 向上取整 select ceil(1.001); # 2 select ceil(1.000); # 1 select ceil(-1.001); # -1 # floor 向下取整 select floor(1.001); # 1 select floor(1.000); # 1 select floor(-1.001); # -2 # truncate 小数点后截断几位 select truncate(1.297, 1); # 1.2 select truncate(1.297, 2); # 1.29 # mod 取余和%同理,符号与被除数一样 select mod(10, -3); # 1 select mod(-10, -3); # -1 select mod(-10, 3); # -1 select 10%3; # 1 复制代码
7.3 日期函数
函数 | 说明 |
now() |
返回当前系统日期和时间 |
curate() 、current_date |
返回当前系统日期,不包括时间 |
curtime() 、current_time |
返回当前时间,不包括日期 |
year() 、month() 、day() 、hour() 、minute() 、second() |
获取时间指定部分,年、月、日、小时、分钟、秒 |
str_todate() |
将日期格式的字符转换成指定格式的日期 |
date_format() |
将日期转换为指定格式字符 |
示例:
# now 返回当前系统日期和时间 select now(); # 2020-07-08 12:29:56 # curdate,current_date 返回当前系统日期,不包括时间 select curdate(); # 2020-07-08 # curtime,current_time 返回当前时间,不包括日期 select curtime(); # 12:29:56 # year... 获取时间指定部分,年、月、日、小时、分钟、秒 select year(now()); # 2020 select month(now()); # 7 select monthname(now()); # July select day(now()); # 8 select dayname(now()); # Wednesday select hour(now()); # 12 select minute(now()); # 29 select second(now()); # 56 select month(order_date) from orders; # str_to_date 将日期格式的字符转换成指定格式的日期 select str_to_date('1-9-2021', '%d-%c-%Y'); # 2020-09-01 select * from orders where order_date = str_to_date('2005-09-01', '%Y-%m-%d'); # date_format 将日期转换成指定格式的字符 select date_format(now(), '%Y年%m月%d日'); # 2020年09月01日 select order_num orderId,date_format(order_date, '%Y/%m') orderMonth from orders; 复制代码
日期格式符:
格式符 | 功能 |
%Y |
四位年份 |
%y |
两位年份 |
%m |
月份(01,02,...12) |
%c |
月份(1,2,...12) |
%d |
日(01,02,...) |
%e |
日(1,2,...) |
%H |
小时(24小时制) |
%h |
小时(12小时制) |
%i |
分钟(00,01,...59) |
%s |
秒(00,01,...59) |
7.4 聚集函数
聚集函数(aggregate function) 运行在行组上,计算和返回单个值的函数。
函数 | 说明 |
avg() |
返回某列的平均值 |
count() |
返回某列的行数 |
max() 、min() |
返回某列最大值、最小值(忽略 null 值) |
sum() |
返回某列之和(忽略 null 值) |
示例:
# 计算产品价格平均值 select avg(prod_price) as avgPrice from products; # 计算供应商id为 1003 提供的产品的平均价格 select avg(prod_price) as avgPrice from products where vend_id = 1003; # 计算价格最大的产品价格 select max(prod_price) as maxPrice from products; # 计算顾客总数 select count(*) from customers; # 计算具有 email 的顾客数 select count(cust_email) from cutomers; # 计算产品价格总和 select sum(prod_price) from products; # 计算订单为 20005 的订单总额 select sum(item_price * quantity) totalPrice from orderitems where order_num = 20005; # 计算产品具有的不同的价格的平均数 select avg(distinct prod_price) avgPrice from products where vend_id = 1003; # 同时计算产品总数、价格最小值、最大值、平均数 select count(*) prodNums, min(prod_price) priceMin, max(prod_price) priceMax, avg(prod_price) priceAvg from products; 复制代码
8. 分组数据
之前的聚集函数都是在 where
子句查询到的所有数据基础上进行的计算,比如查询某个供应商的产品平均价格,但假如希望分别返回每个供应商提供的产品的平均价格,该怎么处理呢。这该用到分组了,分组允许把数据分为多个逻辑组,以便能对每个组进行聚集计算。
8.1 创建分组 group by
使用 group by
子句可以指示 MySQL 按某个数据排序并分组数据,然后对每个组而不是整个结果集进行聚集。
# 分别查询每个供应商提供的产品种类数 select vend_id, count(*) num_prods from products group by vend_id; # 查询每个供应商提供的产品的平均价格 select vend_id, avg(prod_price) avg_price from products group by vend_id; 复制代码
注意:
group by
子句可以包含任意数目的列。这使得能对分组进行嵌套,为数据分组提供更细致的控制。- 如果在
group by
子句中嵌套了分组,数据将在最后规定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。 group by
子句中列出的每个列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在select
中使用表达式,则必须在group by
子句中指定相同的表达式。不能使用别名。- 除聚集计算语句外,
select
语句中的每个列都必须在group by
子句中给出。 - 如果分组列中具有 null 值,则 null 将作为一个分组返回。如果列中有多行 null 值,它们将分为一组。
group by
子句必须出现在where
子句之后,order by
子句之前。
8.2 过滤分组 having
除了能用 group by
分组数据外,MySQL 还允许使用 having
关键字过滤分组,指定包括哪些分组,排除哪些分组。
语法顺序如下:
# 语法顺序 select [查询列表] from [表名] where [筛选条件] group by [分组列表] having [分组后筛选] order by [排序列表] limit [要检索行数]; 复制代码
where
过滤没有分组的概念,指定的是行而不是分组,针对分组的过滤使用 having
子句。事实上,目前为止所学过的所有类型的 where
子句都可以用 having
来替代。
关于 having
和 where
的差别,这里有另一种理解方法,where
在数据分组前进行过滤,having
在数据分组后进行过滤。where
排除的行不包括在分组中,这可能会改变计算值,从而影响 having
子句中基于这些值过滤掉的分组。
能用分组前筛选 where
的,优先考虑分组前筛选。
# 找到提供大于 2 个产品的供应商,并列出其提供的产品数量,这里使用 having 来过滤掉产品数不大于2的供应商 select vend_id, count(*) prodCount from products group by vend_id having prodCount > 2; # 找到供应商提供的商品平均价格大于 10 的供应商,并且按平均价格降序排列 select vend_id, avg(prod_price) avgPrice from products group by vend_id having avgPrice > 10 order by avgPrice desc; 复制代码
9. 子查询
子查询(subquery),嵌套在其他查询中的查询。
9.1 使用子查询进行过滤
当一个查询语句中又嵌套了另一个完整的 select
语句,则被嵌套的 select
语句称为子查询或内查询,外面的 select
语句称为主查询或外查询。
之前所有查询都是在同一张表中的,如果我们想获取的信息分散在两张甚至多张表呢,比如要从订单表 orders 中获取顾客 ID,然后用顾客 ID 去顾客表 custormers 找到对应顾客信息。
# 首先在 orderitems 表中找到产品 TNT2 对应的订单编号 select order_num from orderitems where prod_id = 'TNT2' # 然后在 orders 表中找到订单编号对应的顾客 id select cust_id from orders where order_num in ( select order_num from orderitems where prod_id = 'TNT2'); # 然后去 customers 表中找到顾客 id 对应的顾客名字 select cust_id, cust_name from customers where cust_id in ( select cust_id from orders where order_num in ( select order_num from orderitems where prod_id = 'TNT2')); 复制代码
这里实际上有三条语句,最里边的子查询返回订单号列表,此列表用于其外面的子查询的 where
子句。外面的子查询返回顾客 ID 列表,此顾客 ID 列表用于最外层查询的 where 子句。最外层查询最终返回所需的数据。
对于能嵌套的子查询的数目没有限制,不过在实际使用时由于性能的限制,不能嵌套太多的子查询。
9.2 相关子查询
相关子查询(correlated subquery) 涉及外部查询的子查询。
使用子查询的另一方法是创建计算字段。假如需要显示 customers 表中每个顾客的订单总数。订单与相应的顾客 ID 存储在 orders 表中。
# 首先找到用户 ID 对应的订单数量 select count(*) from orders where cust_id = 10003; # 然后将其作为一个 select 子句,将用户 id select cust_name, cust_state, ( select count(*) from orders where orders.cust_id = customers.cust_id) as order_count from customers order by order_count desc; 复制代码
注意到上面这个 where orders.cust_id = customers.cust_id
,这种类型的子查询叫做相关子查询,任何时候只要列名可能有多义性,就必须使用完全限定语法(表名和列名由一个句点分隔)。
10. 联结表
如果要查的数据分散在多个表中,如何使用单条 select
语句查到数据呢,使用联结可以做到。
联结是一种机制,用来在一条 select
语句中关联表,因此称之为联结。使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。
维护引用完整性 :在使用关系表时,仅在关系列中插入合法的数据非常重要。如果在 products 表中插入拥有没有在 vendors 表中出现的供应商 ID 的供应商生产的产品,则这些产品是不可访问的,因为它们没有关联到某个供应商。
为防止这种情况发生,可指示 MySQL 只允许在 products 表的供应商 ID 列中出现合法值(即出现在 vendors 表中的供应商)。 这就是维护引用完整性,它是通过在表的定义中指定主键和外键来实现的。
10.1 创建联结
联结的创建非常简单,规定要联结的所有表以及它们如何关联即可。
# 列出产品的供应商及其价格 select vend_name, prod_name, prod_price from vendors, products where vendors.vend_id = products.vend_id order by prod_price desc; 复制代码
这里在 where
后面用完全限定列名方式指定 MySQL 匹配 vender 表的 vend_id 列和 products 表的 vend_id 字段。
当引用的列可能有歧义时,必须使用完全限定列名的方式,因为 MySQL 不知道你指的是哪个列。
在联结两个表时,实际上做的是将一个表的每一行与另一个表的每一行配对,所以 where
子句作为过滤条件,过滤出只包含指定联结条件的列 where
vendors.vend_id = products.vend_id
,没有 where 子句,将返回两个表的长度乘积个字段,这叫笛卡尔积(cartesian product),可以运行一下这句看看:
# 返回两个表长度乘积行 select vend_name, prod_name, prod_price from vendors, products; 复制代码
所有联结应该总是使用联结条件,否则会得出笛卡尔积。
10.2 联结多个表
一条 select
语句也可以联结多个表,比如需要把某个订单的产品信息、订单信息、供应商信息都列出来,要找的产品信息分散在供应商、产品、订单信息三个表中。
# 将订单 20005 的产品信息、订单信息、供应商信息找出来 select prod_name, vend_name, prod_price, quantity from orderitems, products, vendors where products.vend_id = vendors.vend_id and orderitems.prod_id = products.prod_id and order_num = 20005; 复制代码
这里使用 and
来连接多个联结条件,定义了 3 个表之间用什么作为关联。
注意:MySQL 在运行时关联多个表以处理联结可能是非常耗费资源的,不要联结不必要的表。联结的表越多,性能下降越厉害。
这里可以使用联结来实现 9.1 节的例子,之前是使用子查询来实现的,从订单表 orders 中获取顾客 ID,然后用顾客 ID 去顾客表 custormers 找到对应顾客信息。
# 使用联结来实现 9.1 的例子 select customers.cust_id, cust_name from orders, customers, orderitems where orders.order_num = orderitems.order_num and customers.cust_id = orders.cust_id and prod_id = 'TNT2'; # 由于三个表中只有一个表有 prod_id,所以不需要限定表名 复制代码
这里提一句,不仅仅列可以起别名,表也可以起,用法跟列的别名一样:
# 把前面这个句子起别名 select c.cust_id, cust_name from orders o, customers c, orderitems oi where o.order_num = oi.order_num and c.cust_id = o.cust_id and prod_id = 'TNT2'; 复制代码
这样不仅仅可以缩短 SQL 语句,也允许在单条 select
语句中多次使用相同的表,同时起的别名不仅可以用在 select
子句,也可以使用在 where
、order by
子句以及语句的其他部分。
10.3 内部联结 inner join
目前为止所用的联结称为等值联结(equijoin),它基于两个表之间的相等测试,也称为内部联结。其实,对于这种联结可以使用稍微不同的语法来明确指定联结的类型。下面的 select
语句返回与前面例子完全相同的数据:
# 列出产品的供应商及其价格 select vend_name, prod_name, prod_price from vendors inner join products on vendors.vend_id = products.vend_id; 复制代码
这里的联结条件使用 on
子句而不是 where
,这两种语法都可以达到效果。尽管使用 where
子句定义联结的确比较简单,但使用明确的联结语法能够确保不会忘记联结条件,有时候这样做也能影响性能。
10.4 自联结
比如某个产品出现了质量问题,现在希望找出这个产品的供应商提供的所有产品信息。按照之前介绍的子查询,我们可以先找到对应产品的供应商,然后找到具有这个供应商 ID 的产品列表:
# 先找到产品 ID 为 TNT1 的供应商 ID,然后找到对应供应商 ID 提供的产品列表 select prod_id, prod_name, vend_id from products where vend_id in ( select vend_id from products where prod_id = 'TNT1' ); 复制代码
使用子查询确实可以实现,使用联结也可以做到,这就是自联结:
# 自联结 select p1.prod_id, p1.prod_name, p1.vend_id from products p1, products p2 where p1.vend_id = p2.vend_id and p2.prod_id = 'TNT1'; 复制代码
自联结查询的两个表是同一个表,因此 products 表需要分别起别名,以作为区分,而且 select
子句中出现的列名也需要限定表明,因为两个表都出现了相同的字段。
自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然最终的结果是相同的,但有时候处理联结远比处理子查询快得多。应该试一下两种方法,以确定哪一种的性能更好。
10.5 自然联结
无论何时对表进行联结,应该至少有一个列出现在不止一个表中(被联结的列)。标准的联结返回所有数据,甚至相同的列多次出现。自然联结排除多次出现,使每个列只返回一次。
自然联结就是你只选择那些唯一的列,这一般是通过对表使用通配符,对所有其他表的列使用明确的子集来完成的。
# 自选择唯一的通配符只对第一个表使用。所有其他列明确列出,所以没有重复的列被检索出来。 select v.*, p.prod_id from vendors v, products p where v.vend_id = p.vend_id; 复制代码
10.6 外部链接 outer join
有些情况下,联结包含了那些在相关表中没有关联行的行,这种类型的联结称为外部联结。
比如:
- 对每个顾客下了多少订单进行计数,包括那些至今尚未下订单的顾客;
- 列出所有产品以及订购数量,包括没有人订购的产品;
- 计算平均销售规模,包括那些至今尚未下订单的顾客。
此时联结需要包含哪些没有关联行的那些行。
比如检索所有用户,及其所下的订单,没有订单的也要列举出来:
# 内部联结,查找用户对应的订单 select c.cust_id, o.order_num from customers c inner join orders o on c.cust_id = o.cust_id; # 左外部联结,将没有下单过的顾客行也列出来 select c.cust_id, o.order_num from customers c left outer join orders o on c.cust_id = o.cust_id; # 右外部联结,列出所有订单及其顾客,这样没下单过的顾客就不会被列举出来 select c.cust_id, o.order_num from customers c right outer join orders o on c.cust_id = o.cust_id; 复制代码
在使用 outer join
语法时,必须使用 right
或 left
关键字指定包括其所有行的表。right
指出的是 outer join
右边的表,而 left
指出的是 outer join
左边的表。上面使用 left outer join
从 from
子句的左边表 custermers 中选择所有行。为了从右边的表中选择所有行,应该使用 right outer join
。
左外部联结可通过颠倒 from
或 where
子句中表的顺序转换为右外部联结,具体用哪个看你方便。
10.7 使用带聚集函数的联结
比如想检索一个顾客下过的订单数量,即使没有也要写 0,此时使用分组和 count
聚集函数来统计数量:
# 找到每个顾客所下订单的数量,并降序排列 select c.cust_id, c.cust_name, count(o.order_num) count_orders from customers c left outer join orders o on c.cust_id = o.cust_id group by c.cust_id order by count_orders desc; 复制代码
因为即使顾客没有下单,也要在结果里,所以把顾客表放在左边,用左外部联结。
11. 组合查询
MySQL 允许执行多条select语句,并将结果作为单个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compound query)。
有两种情况需要使用组合查询:
- 在单个查询中从不同的表返回类似结构的数据;
- 对单个表执行多个查询,按单个查询返回数据。
多数情况下,组合查询可以使用具有多个 where
子句条件的单条查询代替。具体场景可以尝试一下这两种方式,看看对特定的查询哪一种性能更好。
11.1 创建组合查询 union
当查询结果来自于多张表,但多张表之间没有关联,这个时候往往使用组合查询。在每条 select
语句之间放上关键字 union
即可。
# 比如需要列出商品价格小于等于 10 而且是供应商 ID 为 1005 或 1003 的产品信息 select prod_id, prod_name, prod_price, vend_id from products where prod_price <= 10 union select prod_id, prod_name, prod_price, vend_id from products where vend_id in (1005, 1003); # 实际上这句也可以通过 where 语句代替 select prod_id, prod_name, prod_price from products where prod_price <= 10 or vend_id in (1005, 1003); 复制代码
- 有些情况下,比如更复杂的过滤条件、需要从多个表中检索数据的情况下,使用
union
可能会更简单。 union
每个查询必须包含相同的列、表达式、聚集函数,不过每个列不需要以相同的次序列出。- 列数据类型必须兼容,类型不必完全相同,但必须是数据库管理系统可以隐式的转换。
- 组合查询的排序
order by
只能出现在最后一条select
语句之后,不能对不同的select
语句分别排序。
11.2 包含或取消重复的行 union (all)
两行 union
分开的语句可能会返回重复的行,但前面那个例子实际结果却并没有包含重复行,这是因为 union
关键字自动去除了重复的行,如果不希望去重,可以使用 union all
关键字。
# 不去重重复行 select prod_id, prod_name, prod_price, vend_id from products where prod_price <= 10 union all select prod_id, prod_name, prod_price, vend_id from products where vend_id in (1005, 1003); 复制代码
如果需要出现重复行,此时无法使用 where
关键字来达成同样的效果了。
12. 数据的增删改
前面说的都是数据的查询,这一章将所以说数据的增删改。
12.1 数据插入 insert into
数据插入使用 insert
关键字,它可以插入一行、多行数据,也可以插入某些查询的结果。
# 插入一条数据到顾客表中 insert into customers values (null, 'Zhang San', '001 street', 'ShangHai', 'SH', '666666', 'ZH', null, null); 复制代码
这里插入一条数据到顾客表中,存储到每个表列中的数据需要在 values
子句中给出,按照表在创建的时候的顺序依次给出。如果某个列没值就给 null。虽然第一条数据对应 cust_id 列的属性是 not null
的,但是这个列是 auto_increment 也就是自增的,MySQL 会自动忽略你给出的 null 并将值自动增加再填充。
但使用上面 values
子句这种方式并不安全,因为这种方式注入数据完全靠输入数据的顺序,如果表结构变动,就会导致输入数据错位。
安全的数据插入方式是这样的:
# 安全但繁琐的插入方式 insert into customers(cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email) values ('Zhang San', '001 street', 'ShangHai', 'SH', '666666', 'ZH', null, null); 复制代码
这里在前一个括号给出了后面括号中数据对应的列名,这样的话即使表结构或者顺序发生变化,也能正确插入数据。
可以看到列 cust_id 被省略了,当满足下面条件时,列可以省略:
- 列定义为允许 null 值;
- 表定义时这个列给出了默认值,表示如果不给值则使用默认值。
如果不能省略却省略了,会报错。
insert
操作可能很耗时,特别是有很多索引需要更新时,而且它可能降低等待处理的 select 语句的性能。如果数据检索是最重要的,你可以通过在 insert
和 into
之间添加关键字 low_priority
,降低 insert
语句的优先级,这也同样适用于下文提到的 update
和 delete
语句。
12.2 插入多个行
上面介绍的 insert
语句可以一次插入一个行,如果想一次插入多个行,每次都列出列名就比较繁琐了,可以使用下面这种方式:
# 插入多个行 insert into customers(cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email) values ('Zhang San', '001 street', 'ShangHai', 'SH', '666666', 'ZH', null, null), ('Li Si', '002 street', 'BeiJing', 'BJ', '878787', 'ZH', null, '123123@163.com'); 复制代码
values
子句后面继续用括号将字段括起来添加新行,中间加个逗号。这可以提高数据库处理的性能,因为单条 insert
语句处理多个插入比使用多条 insert
语句快。
12.3 插入检索出的数据 insert select
insert
可以将一条 select
语句的结果插入表中,这就是 insert select
。比如你想将另一个表中查询的数据插入到这个表中:
# 从别的表中找出数据,并插入 customers 表中 insert into customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email) select id, name, address, city, state, zip, country, contact, email from custnew; 复制代码
select
的新行也可以省略 cust_id,这样 insert
的时候也是可以自动生成新的 cust_id 的。另外可以看到 select 语句的列名跟 insert into
语句后的列名并不对应,这也没关系,因为 insert select
使用的是位置对应,select 语句返回的第一列对应 cust_id,第二列对应 cust_name,依次对应。select
语句后也可以加入 where
子句进行过滤。
12.4 修改数据 update
update
语句用来修改表中的数据,使用 update
的时候一定要小心,不要忘了添加 where
子句,因为一不小心就会更新表中所有行。
# 更新 id 为 10005 的用户的信息 update customers set cust_email = '888@qq.com' where cust_id = 10005; 复制代码
如果这里没有使用 where
子句,update
将会更新这个表中的所有行的 cust_email 字段,所以一定要注意。
要删除某行某列的值,可以将值修改为 null。
更新多个字段的方式也很简单:
# 更新多个字段 update customers set cust_email = '666@qq.com', cust_contact = 'S Zhang' where cust_id = 10005; 复制代码
如果用 update
语句更新多行,并且在更新这些行中的一行或多行时出一个现错误,则整个 update
操作被取消 (错误发生前更新的所有行被恢复到它们原来的值)。为即使是发生错误,也继续进行更新,可以在 update
后使用 ignore
关键字。
update
语句可以使用子查询,用 select
语句检索出的数据来更新列数据。
12.5 删除数据 delete
delete
语句可以用来从表中删除特定的行或者所有行。使用 delete
语句的时候要小心,不要忘了添加 where
子句,因为一不小心就会删除表中所有行。
# 删除顾客表中顾客 id 为 10008 的行 delete from customers where cust_id = 10008; 复制代码
如果将 where
子句去掉,那么就是删除这个表中的所有行,但不是删除这个表,删除表使用的是另一个语句 drop
。另外删除一个表中所有行更快的语句是 truncate table
,因为 delete
是逐行删除数据,而 truncate
是删除原来的表重新建个表。
注意,在使用 update
和 delete
之前,应该非常小心,因为 MySQL 没有撤销,使用之前建议先使用 select
进行测试,防止 where
子句不正确导致数据丢失。
13. 创建和操作表
13.1 创建表 create table
我们可以打开之前为了整点数据执行的 create.sql 文件看看,用 VSCode/Webstorm/Navivate/文本 都能打开,这个文件除了注释的第一行就是创建表语句:
CREATE TABLE customers ( cust_id int NOT NULL AUTO_INCREMENT, cust_name char(50) NOT NULL , cust_address char(50) NULL , cust_city char(50) NULL , cust_state char(5) NULL , cust_zip char(10) NULL , cust_country char(50) NULL DEFAULT 'ZH', # 指定默认值 cust_contact char(50) NULL , cust_email char(255) NULL , PRIMARY KEY (cust_id) # 指定主键 ) ENGINE=InnoDB; 复制代码
从这里可以看到 create table
的格式。
如果要在一个表不存在时创建,应该在表名前、create table
后加上 if not exists
。这样会先检查表名是否已存在,并且在不存在时进行创建。
对于 auto_increment
,每个表只能有一个 auto_increment
,而且它必须被索引。当你使用 insert
语句插入一个新的值,后续自动增量将从这个值重新开始增加。如果一个表创建新的列需要得到最 auto_increment
的值,可以使用 last_insert_id()
来获取最后自增的值。
上面创建语句的列名后 null
表示这个列在插入和修改时允许不给出值,如果是 not null
,那么在插入或修改时就必须给值,否则会报错。默认为 null
,如果不显式的给出 not null
,则会默认为 null
。
primary key
指示主键的值,在插入时主键值必须是不重复的,主键也可以是多个字段 primary key (cust_id, cust_name)
用逗号分开。作为主键的列是不能允许 null 的。
default
关键字可以指定默认值,如果插入行没有指定默认值,那么将默认使用默认值。
最后的 engine
字段指定了不同的引擎,以下是 MySQL 支持的几个常用的引擎:
- InnoDB 可靠的事务处理引擎,不支持全文搜索。
- MEMORY 功能等同于 MyISAM,但由于数据存储在内存,速度很快,适合于临时表。
- MyISAM 默认是这个,性能高,支持全文搜索,不支持事务。
根据不同需要可以选择不同引擎。
13.2 修改表 alter table
修改表使用 alter table
语句,一般情况下,当表中开始存储数据后,就不应该再修改表。所以表在设计时需要大量时间来考虑,尽量在后期不对表进行大的改动。
# 给供应商表增加一列 vend_phone 字段 alter table vendors add vend_phone char(20) default 12212341234; # 删除这个添加的 vend_phone 字段 alter table vendors drop column vend_phone; 复制代码
alter table
经常用来定义外键 foreign key
,用于限制两个表的关系,保证该字段的值必须来自于主表的关联列的值。外键具有保持数据完整性和一致性的机制,对业务处理有着很好的校验作用。
外键用来在从表添加外键约束,用于引用主表中某列的值,比如学生表的专业编号,员工表的部门编号,员工表的工种编号,产品表的供应商编号等等。
可以从 create.sql 文件最下面看到外键的例子,这里列举一行:
# 将订单信息表的 order_num 设置为订单表的外键 alter table orderitems add constraint fk_orderitems_orders foreign key (order_num) references orders (order_num); 复制代码
设置外键之后,如果外键已经有对应数据,就不能直接删除主表的这个外键行了:
# 直接删除外键行报错,不允许删 delete from orders where order_num = 20009; # 先删除 orderitems 中的关联行再删除 orders 中的外键行,就可以删了 delete from orders where order_num = 20009; delete from orderitems where order_num = 20009; 复制代码
所以在插入数据时,先插入主表,再插入从表。删除数据时,先删除从表,再删除主表。
注意:使用 alter table
要小心,最好在改动之前做一个完整的备份,数据库表的更改不能撤销。如果不小心增加了不需要的列,可能无法删除它们,如果删除了不该删除的列,可能就丢失了数据。
13.3 删除表 drop table
删除一个表可以使用 drop table
关键字。
drop table customers2; 复制代码
删除表没有确认,也没有撤销,执行后将永久删除该表。
如果删除时不存在这个表会报错,可以在 drop table
关键字后加上 if exists
,这样数据库会先检查这个目标表是不是存在:
# 删除一个表,如果没加 if exists 表又不存在则会报错 drop table if exists customers2; 复制代码
创建新表时,指定的表名必须不存在,否则会报错,所以在创建前也可以执行这个句子。
13.4 重命名表 rename table
重命名一个表可以使用 rename table
关键字。
# 重命名一个表 rename table customers to customers2; # 重命名多个表 rename table customers to customers2, vendors to vendors2; 复制代码
14. 视图
视图是虚拟存在的表,行和列的数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的,只保存 SQL 逻辑,不保存查询结果。
14.1 创建视图 create view
比如说现在要查询购买了 TNT2 产品的顾客信息,按之前介绍的知识使用联结从三个表中查找:
# 找到购买了 TNT2 的顾客信息 select cust_name, cust_contact, cust_email, prod_id from customers c, orders o, orderitems oi where c.cust_id = o.cust_id and o.order_num = oi.order_num and prod_id = 'TNT2'; 复制代码
那如果现在要换成找到购买了另一个产品的顾客信息呢,重新写一遍查询语句似乎有点重复。程序员永远不做重复的事,如果有一个虚拟表,名为 prod_cust,然后使用 select * from prod_cust where prod_id = 'TNT1'
就可以轻松找到对应的行了,这就是视图。
# 创建视图 create view prod_cust as select cust_name, cust_contact, cust_email, prod_id from customers c, orders o, orderitems oi where c.cust_id = o.cust_id and o.order_num = oi.order_num; # 使用视图查询购买了产品 TNT2 的顾客信息 select cust_name, cust_email from prod_cust where prod_id = 'TNT2'; 复制代码
使用起来挺简单的,可以根据需要编写出可重复使用的视图,方便查询。
视图的使用如下:
create view prod_cust as ...; # 创建视图 show create view prod_cust; # 查看创建视图的语句 drop view prod_cust; # 删除视图 create or replace view prod_cust as ...; # 更新视图 复制代码
如果要修改视图,可以先删除再新建,也可以直接 create or replace view
,如果存在则会替换,如果不存在则会新建。
视图并不直接包含数据,而是一个 SQL 查询。视图和普通表的关系,就像临时组建的歌唱团和普通班级的关系。
但也因为视图不包含数据,每次都要重新执行,所以如果使用视图的场景比较复杂比如嵌套视图等,那么你会发现性能下降的厉害,所以在大量使用视图的场景可能需要关注性能问题。
视图创建后,可以像使用表一样使用视图,对视图进行 select
、过滤、排序、联结等等操作。
使用视图可以:
- 复用 SQL 语句。
- 简化复杂的 SQL 操作。在编写查询后,可以方便地重用它而不必知道它的基本查询细节。
- 使用表的组成部分而不是整个表。
- 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。
- 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。
顺便说一句,创建视图之后,show tables
也会显示视图,所以你可以通过下面方式查询所有基表或者视图:
# 显示当前database中不包括视图的所有基表 select table_name from information_schema.tables where table_schema = 'mysql_demo1' and table_type = 'BASE TABLE'; # 显示当前database中的所有视图 select table_name from information_schema.tables where table_schema = 'mysql_demo1' and table_type = 'VIEW'; 复制代码
14.2 使用视图重新格式化检索出的数据
比如某个场景,经常会使用到一些格式化的数据,那么就可以使用视图把数据格式化的形式先拼接好:
# 经常用到的供应商信息,可以先组装成视图 create or replace view vend_infos as select vend_id, concat(vend_name, '(', vend_city, ', ', vend_country, ')') vend_info from vendors; # 使用视图直接拿到拼好的供应商信息 select prod_id, prod_name, vend_info from products, vend_infos where products.vend_id = vend_infos.vend_id; 复制代码
14.3 使用视图过滤不想要的数据
比如某个场景,需要找到邮箱地址不为 null 的顾客下的订单:
# 找到 email 不是 null 的顾客下的订单 select order_num, cust_name, cust_email from customers, orders where customers.cust_id = orders.cust_id and cust_email is not null; 复制代码
但是另一个场景又需要找到邮箱地址不为 null 的顾客购买的所有商品列表,此时我们可以使用视图,把邮箱地址不为 null 的顾客创建为一个视图,在其他场景使用:
# 创建邮箱地址不为 null 的顾客的视图 create view cust_has_email as select * from customers where cust_email is not null; # 找到 email 不是 null 的顾客下的订单 select order_num, cust_name, cust_email from cust_has_email c, orders o where c.cust_id = o.cust_id; # 找到 email 不是 null 的顾客购买的所有商品列表 select c.cust_name, c.cust_email, c.cust_name, prod_name from cust_has_email c, orders o, orderitems oi, products p where c.cust_id = o.cust_id and oi.order_num = o.order_num and p.prod_id = oi.prod_id; 复制代码
可以看到视图这里就完全被当成了一个表来和其他表一起使用。
14.4 使用视图与计算字段
视图对于简化计算字段的使用很有用,比如希望查找 20008 的订单的订单总额:
# 查找 20008 订单的订单总额 select order_num, sum(quantity * item_price) sum_price from orderitems where order_num = 20008; 复制代码
那么希望查找另一个订单总额时,可以使用视图来改造一下:
# 查找订单总额视图 create or replace view sum_orders as select order_num, sum(quantity * item_price) sum_price from orderitems group by order_num; # 找另一个订单的总金额 select order_num, sum_price from sum_orders where order_num = 20009; 复制代码
看到视图十分好用,实际使用中按需使用视图可以极大方便数据库操作。
14.5 更新视图
视图也是可以使用 insert
、update
、delete
更新数据的,虽然视图只是一个 SQL 句子而不是实际数据。
更新视图的数据会更新其基表,但并非所有视图都可以更新的,如果数据库不冷确定被更新的基数据,则不允许更新。比如分组、联结、子查询、并、聚集函数、distinct
等等。
# 比如给上面的 email 不为 null 的视图添加一行数据,插入会成功 insert into cust_has_email(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact) values (10010, 'Zhang San', '001 street', 'ShangHai', 'SH', '9999', 'ZH', 'Li S'); # 给查找订单总额视图增加一行会失败,因为这里有分组,数据库不知道在哪插入 insert into sum_orders(order_num, order_item, prod_id, quantity, item_price) values (20009, 5, 'OL1', 2, 8.99); 复制代码
不过一般,应该把视图用于检索数据 select
,而不是增删改数据 insert
、update
、delete
。
15. 存储过程
前面介绍的大部分 SQL 语句都是对一个或者多个表的单个查询,但是实际情况下一个完整的操作可能是由多个语句组合而成的,比如考虑下面这个下单流程:
- 为了处理订单,需要核对以保证库存中有相应的物品。
- 如果库存有物品,这些物品需要预定以便不将它们再卖给别的人,并且要减少可用的物品数量以反映正确的库存量。
- 库存中没有的物品需要订购,需要与供应商进行一些交互。
- 关于哪些物品入库(并且可以立即发货)和哪些物品退订,需要通知相应的客户。
可以说存储过程就是数据库 SQL 语言层面上的代码封装和重用,可以回传值,也可以接受参数。可以将其视为批文件,但作用不仅限于批处理。
存储过程简单、安全、高性能。不过有些数据库管理员会限制存储过程的创建权限,只允许用户使用,但不允许用户创建存储过程。