MySQL 从入门到实践,万字详解!(中)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 数据库是往全栈发展不得不跨过的一道坎,大家不可避免会学到用到相关知识,最近查资料的时候发现网上很多内容要么就特别深,要么不成体系,对一些希望浅尝辄止仅仅是使用一下的人不太友好。最近刚好有机会学到 MySQL,集中一些时间学习了一下 MySQL 同时做了一些笔记,每个概念基本都有代码示例,每一行都是在下手打,读者可以直接复制了代码到命令行中运行,希望对大家有所帮助~ 😜

7. 数据处理函数


前面介绍的去除数据首位空格的 trim() 函数就是数据处理函数,除此之外还有多种其他类型的数据处理函数:


  1. 用于处理文本串的文本函数,如删除或填充值,转换值为大写或小写。
  2. 用于在数值数据上进行算术操作的数值函数,如返回绝对值,进行代数运算。
  3. 用于处理日期和时间值并从这些值中提取特定成分的日期和时间函数,例如,返回两个日期之差,检查日期有效性等。
  4. 系统函数,如返回用户登录信息,检查版本细节。


在不了解如何使用一个函数的时候,可以使用 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;
复制代码


注意:


  1. group by 子句可以包含任意数目的列。这使得能对分组进行嵌套,为数据分组提供更细致的控制。
  2. 如果在 group by 子句中嵌套了分组,数据将在最后规定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。
  3. group by 子句中列出的每个列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 select 中使用表达式,则必须在 group by 子句中指定相同的表达式。不能使用别名。
  4. 除聚集计算语句外,select 语句中的每个列都必须在 group by 子句中给出。
  5. 如果分组列中具有 null 值,则 null 将作为一个分组返回。如果列中有多行 null 值,它们将分为一组。
  6. 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 来替代。


关于 havingwhere 的差别,这里有另一种理解方法,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 子句,也可以使用在 whereorder 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 语法时,必须使用 rightleft 关键字指定包括其所有行的表。right 指出的是 outer join 右边的表,而 left 指出的是 outer join 左边的表。上面使用 left outer joinfrom 子句的左边表 custermers 中选择所有行。为了从右边的表中选择所有行,应该使用 right outer join


左外部联结可通过颠倒 fromwhere 子句中表的顺序转换为右外部联结,具体用哪个看你方便。


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)。


有两种情况需要使用组合查询:


  1. 在单个查询中从不同的表返回类似结构的数据;
  2. 对单个表执行多个查询,按单个查询返回数据。


多数情况下,组合查询可以使用具有多个 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);
复制代码


  1. 有些情况下,比如更复杂的过滤条件、需要从多个表中检索数据的情况下,使用 union 可能会更简单。
  2. union 每个查询必须包含相同的列、表达式、聚集函数,不过每个列不需要以相同的次序列出。
  3. 列数据类型必须兼容,类型不必完全相同,但必须是数据库管理系统可以隐式的转换。
  4. 组合查询的排序 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 被省略了,当满足下面条件时,列可以省略:


  1. 列定义为允许 null 值;
  2. 表定义时这个列给出了默认值,表示如果不给值则使用默认值。


如果不能省略却省略了,会报错。


insert 操作可能很耗时,特别是有很多索引需要更新时,而且它可能降低等待处理的 select 语句的性能。如果数据检索是最重要的,你可以通过在 insertinto 之间添加关键字 low_priority ,降低 insert 语句的优先级,这也同样适用于下文提到的 updatedelete 语句。


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 是删除原来的表重新建个表。


注意,在使用 updatedelete 之前,应该非常小心,因为 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 支持的几个常用的引擎:


  1. InnoDB 可靠的事务处理引擎,不支持全文搜索。
  2. MEMORY 功能等同于 MyISAM,但由于数据存储在内存,速度很快,适合于临时表。
  3. 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、过滤、排序、联结等等操作。


使用视图可以:


  1. 复用 SQL 语句。
  2. 简化复杂的 SQL 操作。在编写查询后,可以方便地重用它而不必知道它的基本查询细节。
  3. 使用表的组成部分而不是整个表。
  4. 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。
  5. 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。

顺便说一句,创建视图之后,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 更新视图


视图也是可以使用 insertupdatedelete  更新数据的,虽然视图只是一个 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,而不是增删改数据 insertupdatedelete


15. 存储过程


前面介绍的大部分 SQL 语句都是对一个或者多个表的单个查询,但是实际情况下一个完整的操作可能是由多个语句组合而成的,比如考虑下面这个下单流程:


  1. 为了处理订单,需要核对以保证库存中有相应的物品。
  2. 如果库存有物品,这些物品需要预定以便不将它们再卖给别的人,并且要减少可用的物品数量以反映正确的库存量。
  3. 库存中没有的物品需要订购,需要与供应商进行一些交互。
  4. 关于哪些物品入库(并且可以立即发货)和哪些物品退订,需要通知相应的客户。


可以说存储过程就是数据库 SQL 语言层面上的代码封装和重用,可以回传值,也可以接受参数。可以将其视为批文件,但作用不仅限于批处理。


存储过程简单、安全、高性能。不过有些数据库管理员会限制存储过程的创建权限,只允许用户使用,但不允许用户创建存储过程。



相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
安全 关系型数据库 MySQL
PHP与MySQL交互:从入门到实践
【9月更文挑战第20天】在数字时代的浪潮中,掌握PHP与MySQL的互动成为了开发动态网站和应用程序的关键。本文将通过简明的语言和实例,引导你理解PHP如何与MySQL数据库进行对话,开启你的编程之旅。我们将从连接数据库开始,逐步深入到执行查询、处理结果,以及应对常见的挑战。无论你是初学者还是希望提升技能的开发者,这篇文章都将为你提供实用的知识和技巧。让我们一起探索PHP与MySQL交互的世界,解锁数据的力量!
|
2月前
|
关系型数据库 MySQL 数据库
MySQL基本操作入门指南
MySQL基本操作入门指南
84 0
|
10天前
|
关系型数据库 MySQL Linux
Linux环境下MySQL数据库自动定时备份实践
数据库备份是确保数据安全的重要措施。在Linux环境下,实现MySQL数据库的自动定时备份可以通过多种方式完成。本文将介绍如何使用`cron`定时任务和`mysqldump`工具来实现MySQL数据库的每日自动备份。
28 3
|
9天前
|
存储 监控 关系型数据库
MySQL自增ID耗尽解决方案:应对策略与实践技巧
在MySQL数据库中,自增ID(AUTO_INCREMENT)是一种特殊的属性,用于自动为新插入的行生成唯一的标识符。然而,当自增ID达到其最大值时,会发生什么?又该如何解决?本文将探讨MySQL自增ID耗尽的问题,并提供一些实用的解决方案。
16 1
|
24天前
|
NoSQL 关系型数据库 MySQL
MySQL与Redis协同作战:百万级数据统计优化实践
【10月更文挑战第21天】 在处理大规模数据集时,传统的单体数据库解决方案往往力不从心。MySQL和Redis的组合提供了一种高效的解决方案,通过将数据库操作与高速缓存相结合,可以显著提升数据处理的性能。本文将分享一次实际的优化案例,探讨如何利用MySQL和Redis共同实现百万级数据统计的优化。
61 9
|
1月前
|
消息中间件 监控 关系型数据库
MySQL数据实时同步到Elasticsearch:技术深度解析与实践分享
在当今的数据驱动时代,实时数据同步成为许多应用系统的核心需求之一。MySQL作为关系型数据库的代表,以其强大的事务处理能力和数据完整性保障,广泛应用于各种业务场景中。然而,随着数据量的增长和查询复杂度的提升,单一依赖MySQL进行高效的数据检索和分析变得日益困难。这时,Elasticsearch(简称ES)以其卓越的搜索性能、灵活的数据模式以及强大的可扩展性,成为处理复杂查询需求的理想选择。本文将深入探讨MySQL数据实时同步到Elasticsearch的技术实现与最佳实践。
86 0
|
2月前
|
SQL 关系型数据库 MySQL
MySQL入门到精通
MySQL入门到精通
|
3月前
|
API C# 开发框架
WPF与Web服务集成大揭秘:手把手教你调用RESTful API,客户端与服务器端优劣对比全解析!
【8月更文挑战第31天】在现代软件开发中,WPF 和 Web 服务各具特色。WPF 以其出色的界面展示能力受到欢迎,而 Web 服务则凭借跨平台和易维护性在互联网应用中占有一席之地。本文探讨了 WPF 如何通过 HttpClient 类调用 RESTful API,并展示了基于 ASP.NET Core 的 Web 服务如何实现同样的功能。通过对比分析,揭示了两者各自的优缺点:WPF 客户端直接处理数据,减轻服务器负担,但需处理网络异常;Web 服务则能利用服务器端功能如缓存和权限验证,但可能增加服务器负载。希望本文能帮助开发者根据具体需求选择合适的技术方案。
159 0
|
3月前
|
SQL 关系型数据库 MySQL
(二十五)MySQL主从实践篇:超详细版读写分离、双主热备架构搭建教学
在上篇《主从原理篇》中,基本上把主从复制原理、主从架构模式、数据同步方式、复制技术优化.....等各类细枝末节讲清楚了,本章则准备真正对聊到的几种主从模式落地实践,但实践的内容通常比较枯燥乏味,因为就是调整各种配置、设置各种参数等步骤。
549 2
|
3月前
|
存储 关系型数据库 MySQL
深入MySQL:事务日志redo log详解与实践
【8月更文挑战第24天】在MySQL的InnoDB存储引擎中,为确保事务的持久性和数据一致性,采用了redo log(重做日志)机制。redo log记录了所有数据修改,在系统崩溃后可通过它恢复未完成的事务。它由内存中的redo log buffer和磁盘上的redo log file组成。事务修改先写入buffer,再异步刷新至磁盘,最后提交事务。若系统崩溃,InnoDB通过redo log重放已提交事务并利用undo log回滚未提交事务,确保数据完整。理解redo log工作流程有助于优化数据库性能和确保数据安全。
556 0