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

Posted SHERlocked93

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL 从入门到实践,万字详解!相关的知识,希望对你有一定的参考价值。

数据库是往全栈发展不得不跨过的一道坎,大家不可避免会学到用到相关知识,最近查资料的时候发现网上很多内容要么就特别深,要么不成体系,对一些希望浅尝辄止仅仅是使用一下的人不太友好。

最近刚好有机会学到 mysql,集中一些时间学习了一下 MySQL 同时做了一些笔记,每个概念基本都有代码示例,每一行都是在下手打,读者可以直接复制了代码到命令行中运行,希望对大家有所帮助~  ????

本文介绍的知识都不是特别深,目标用户是对 MySQL 零基础或弱基础的小伙伴们,可以帮助对 MySQL 建立一些概念,至少碰到相关问题知道怎么去百度,也不会碰到后端给的数据库文件看不懂。

对于 Docker 和 CentOS 相关知识不了解的小伙伴可以看看 <手摸手带你 Docker 从入门到实践>[1] 和 <半小时搞会 CentOS 入门必备基础知识>[2] 两篇文章,反正 Docker 和 CentOS 也早晚会用到 ????

所有代码保存在 Github[3] 上,可以自行 Clone 下来阅读和执行。

CentOS 版本:7.6

MySQL 版本:8.0.21

上面这个脑图可以加文末公众号回复 「mysql脑图」 获取 xmind 源文件。

1. 什么是数据库

数据库是一个以某种有组织的方式存储的数据集合,可以将其想象为一个文件柜。

1.1 基本信息

MySQL 数据库隶属于MySQL AB公司,总部位于瑞典,后被 oracle 收购。是目前最流行的关系型数据库。

优点:

  1. 成本低:开放源代码,一般可以免费试用;

  2. 性能高:执行很快;

  3. 简单:很容易安装和使用。

1.2 MySQL 安装

MySQL 建议使用 Docker 安装,几行命令就安装好了,参见 <手摸手带你 Docker 从入门到实践> - 安装 MySQL[4]

我这里的命令是:

# 创建mysql容器
docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=888888 \\
-v /Users/sherlocked93/Personal/configs/mysql.d:/etc/mysql/conf.d \\
-v /Users/sherlocked93/Personal/configs/data:/var/lib/mysql \\
--name localhost-mysql mysql

# 创建好之后进入 mysql 容器:
docker exec -it localhost-mysql bash

# 登录 mysql
mysql -u root -p888888

如果你机子上安装了 navicate,可以参考一下下面这个配置

选择 New Connection

选择 New Connection 之后填一下配置:

navicat配置

就可以看到你数据库里面的内容了。

就可以啦,效果如下图:

不用 Docker 可以去官网 MySQL Community Server[5] 下载对应版本的 MySQL 安装包,Community Server 社区版本是不要钱的,下载安装完毕也可以,基本一直下一步就行了。

废话少说,下面直接开始知识灌体!

2. MySQL  简单使用

2.1 数据库相关术语

数据库相关的概念和术语:

  1. 数据库(database) 保存有组织的数据的容器;

  2. (table) 某种特定类型数据的结构化清单;

  3. 模式(schema) 关于数据库和表的布局及特性的信息;

  4. (column) 表中的一个字段,所有表都是由一个或多个列组成的;

  5. 数据类型(datatype) 所容许的数据的类型;

  6. (row) 表中的一个记录;

  7. 主键(primary key) 一列(或一组列),其值能够唯一区分表中每个行;

  8. 外键(foreign key) 表中的一列,它包含另一个表的主键值,定义了两个表之间的关系。

  9. 子句(clause) SQL 语句由子句构成,有些子句是必需的,而有的是可选的。比如 select 语句的 from 子句。

2.2 主键

主键的概念十分重要,它唯一标识表中每行的单个或者多个列称为主键。主键用来表示一个特定的行。

虽然并不总是都需要主键,但应尽量保证每个表都定义有主键,以便于以后的数据操纵和管理。没有主键,无法将不同的行区分开来,更新或删除表中特定行很困难。

表中的任何列都可以作为主键,只要它满足以下条件:

  1. 任意两行都不具有相同的主键值;

  2. 每个行都必须具有一个主键值(主键列不允许 NULL 值)。

在使用多列作为主键时,上述条件必须应用到构成主键的所有列,所有列值的组合必须是唯一的(单个列的值可以不唯一)。

几个普遍认可的最好习惯为:

  1. 不更新主键列中的值;

  2. 不重用主键列的值;

  3. 不在主键列中使用可能会更改的值。

2.3 语法规范

语法规范:

  1. 输入 help\\h 获取帮助;

  2. 不区分大小写,但建议关键字大写,表名、列名小写;

  3. 每条命令最好使用分号 ;\\g 结尾,仅按 Enter 不执行命令;

  4. 每条命令根据需要,可以进行缩进、换行;

  5. # 开头进行多行注释,用 /* ... */ 进行多行注释;

  6. 输入 quitexit 推出 MySQL 命令行;

语法特点:

  1. 大小写不敏感;

  2. 可以写在一行或多行,可以分成多行以便于阅读和调试;

  3. 关键字不能被缩写也不能分行;

  4. 各子句一般分行写;

  5. 推介使用缩进提高语句的可读性;

常见的简单命令:

mysql -u root -p      # –h 主机名 –u 用户名 -P 端口号 –p密码,注意-p跟密码之间不能加空格其他可以加可以不加
select version();     # 查看 mysql 服务版本
show databases;       # 查看所有数据库,注意最后有 s

create database [库名]; # 创建库
use [库名];             # 打开指定的库

show tables;           # 查看当前库的所有表        
show tables from [库名];   # 查看其他库的所有表               
desc [表名];               # 查看表结构   

create table [表名] (      # 创建表            
	列名 列类型,
	列名 列类型,
);

drop database [库名];     # 删除库  
drop table [表名];        # 删除表   

exit;                    # 退出

2.4 创建表并填充数据

首先我们整点数据,方便后面的代码演示。

mysql -u root -p888888     # 输入用户名密码,进入mysql命令行

然后在 Github 下载文件 create.sql[6] 并运行(也可以直接复制文件里的内容到 MySQL 命令行中执行)。

如果你用的是 navicate,在上一章创建到 localhost-mysql 的连接后,运行一下即可:

同理运行另一个文件 populate.sql[7],填充每个表中的数据。

运行之后在命令行中 show tables 就可以看到库中的表了,如下图:

2.5 关系表

简单介绍一下刚刚创建好的表。

为了数据分类处理,顾客 customers、供应商 vendors、订单 orders、订单信息 orderitems、产品记录 productnotes、产品 products 表分别存储不同的信息。

比如供应商信息表 vendors 总每个供应商都有一个唯一标识,也就是主键 vend_id,而 products 产品表的每个产品也有一个主键 prod_id,还有一个字段 vend_id 供应商 ID 和供应商表中的 vend_id 一一对应,这就是外键

如果你希望通过产品 ID 查到对应的供应商信息,那么就通过外键来找到另一个表中的信息。外键避免了每个产品都重复保存供应商的详细信息,只要保存供应商的 ID 就行,当供应商信息变了,比如邮箱、地址变更,也不用挨个改每一行的数据,只需更改供应商表中对应供应商信息。

这样做的好处:

  1. 供应商信息不重复,从而不浪费时间和空间;

  2. 如果供应商信息变动,可以只更新 vendors 表中的单个记录,相关表中的数据不用改动;

  3. 由于数据无重复,显然数据是一致的,这使得处理数据更简单。

可伸缩性(scale),能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称之为可伸缩性好(scale well)。

2.6 数据类型

MySQL 数据类型定义了列中可以存储什么数据以及该数据怎样存储的规则。

数值型

整型:TinyintSmallintMediumintIntInteger)、Bigint,可以为无符号和有符号,默认有符号。

  1. 如果不设置有无符号默认是有符号,如果想设置无符号,可以添加 unsigned 关键字;

  2. 如果插入的数值超出了整型的范围,会报 out of range 异常,并且插入临界值;

  3. 如果不设置长度,会有默认的长度。

小数

  1. 定点数:dec(M,D)decimal(M,D)

  2. 浮点数:float(M, D)double(M, D)

M 为整数部位+小数部位,D 为小数部位,M 和 D 都可以省略。如果是 decimal,则 M 默认为 10,D 默认为 0。

字符型

  1. 较短的文本:char(n)varchar(n) 中的 n 代表字符的个数,不代表字节个数。

  2. 较长的文本:text(长文本数据)、blob(较长的二进制数据)。

  3. binaryvarbinary 用于保存较短的二进制。

  4. enum 用于保存枚举。

  5. set 用于保存集合。

日期和时间类型

  1. date 格式 YYYY-MM-DD,保存日期;

  2. time 格式 HH:MM:SS,保存时间;

  3. year 格式 YYYY,保存年;

  4. datetime 格式 YYYY-MM-DD HH:MM:SS,保存日期+时间,范围 1000-9999,不受时区印象;

  5. timestamp 时间戳,格式保存日期+时间,范围 1970-2038,受时区影响;

3. 检索数据 select

用来查询的 select 语句大概是最常用的了,用来从一个或多个表中检索信息,一条 select 语句必须至少给出两条信息:想选择什么、从什么地方选择。

# 基本语法
select [查询列表] from [表名];

# 查询单个/多个/所有字段
select cust_name from customers;
select cust_name,cust_city,cust_address from customers;
select `select` from customers;               # 如果某字段是关键字,可以用 ` 符号包起来
select * from customers;                      # * 表示所有                

# 查询常量值/表达式/函数
select 100;
select 'zhangsan';
select 100%98;
select version();

3.1 去重 distinct

查询出来的结果可能有多个重复值,可以使用 distinct 关键字来去重

select order_num from orderitems;           # 有一些重复值
select distinct order_num from orderitems;  # 将重复值去重

3.2 限制结果 limit

select 语句返回所有匹配的行,它们可能是指定表中的每个行。为了返回第一行或前几行,可使用 limit 子句。

limit m 表示返回找出的前 m 行,limit m,n 表示返回第 m 行开始的 n 行,也可以使用 limit m offset n 含义和前面一样。

注意,检索出来的第一行的索引为 0 行。

3.3 完全限定表名与列名

在某些情况下,语句可能使用完全限定的列明与表名:

select orderitems.order_num from mysql_demo1.orderitems;
# 上面这句等价于
select order_num from orderitems;

4. 排序检索数据 order by

上一章从 orderitems 这个表中检索的数据是没有排序的,一般情况下返回的顺序是在底层表中出现的顺序。可以通过 order by 子句来对检索后的数据进行排序。

可以用 ascdesc 关键字来指定排序方向。order by asc 升序、order by desc 降序,不写默认是升序。

order by 子句中可以支持单个字段、多个字段、表达式、函数、别名,一般放在句子的最后面,除了 limit 之外。

select * from orderitems order by item_price;           # 按item_price升序排列

# 先按 quantity 升序排列,quantity 的值一样时按 item_price 的值升序排列
select * from orderitems order by quantity,item_price;  

# 先按 quantity 降序排列,quantity 的值一样时按 item_price 的值升序排列
select * from orderitems order by quantity desc,item_price;  

# 找到最贵订单
select * from orderitems order by item_price desc limit 1;

5. 过滤数据 where

from 子句后使用 where 关键字可以增加筛选条件,过滤数据。

# 基本语法
select [查询列表] from [表名] where [筛选条件] order by [排序条件];

按条件表达式来筛选 >=<>=<=!=<><=>

# 找出产品价格为 2.5 的产品名字
select prod_name, prod_price from products where prod_price=2.5;

# 找出产品价格小于等于 10 的产品名字,并按产品价格降序排列
select prod_name, prod_price from products where prod_price <= 10 order by prod_price desc;

# 找到供应商 id 不为 1003 的产品,!= 和 <> 含义一样,都是不等于
select vend_id, prod_name from products where vend_id <> 1003;

5.1 范围检查 between and

使用 between ... and ... 指定所需范围的开始值和结束值,可以达到范围查询的效果。

注意 between and 左右数字是按小大的顺序的,调过来不行。

# 查询产品价格在 3 到 10 内的产品
select prod_name, prod_price from products where prod_price between 3 and 10;

# 单独使用 and 也可以打到这个效果
select prod_name, prod_price from products where prod_price <= 10 and prod_price >= 3;

5.2 空值检查 is (not) null

创建表时,可以指定某些列可以不包含值,即可以为 nullnull 表示无值 no value,这与字段包含 0、空字符串或仅仅包含空格不同。

使用 is nullis not null 可以用来判断一个值是否为 null

说明:

  1. 等于 = 和不等于 <>!= 不能用来判断 null,只能用 is nullis not null 来判断 null

  2. <=> 安全等于号可以用来判断 null

# 找出顾客中邮箱不是 null 的顾客信息
select * from customers where cust_email is not null;

# 使用安全等于号来判断 null 也是可以的
select * from customers where cust_email <=> null;

5.3 逻辑与操作符 and

操作符(operator) 用来联结或改变 where 子句中的子句的关键字,也称为逻辑操作符(logical operator)。

前文提到了 and 操作符,通过这个操作符可以增加限定条件:

# 找出供应商为 1003 提供的价格小于等于 10 的产品
select * from products where vend_id = 1003 and prod_price <= 10;

5.4 逻辑或操作符 or

or 操作符和 and 操作符相反,这是逻辑或操作符,返回匹配任一条件的行:

# 找出id为 1003 或 1001 的供应商
select * from products where vend_id = 1003 or vend_id = 1001;

andor 同时出现时,会优先处理 and,比如这句:

select * from products where vend_id = 1001 or vend_id = 1003 and prod_price >= 10;

这句会先处理 and,表示 vend_id 为 1003 且 prod_price 大于等于 10 的产品,或者 vend_id 为 1001 的产品。

遇到这种情况,可以通过增加圆括号:

select * from products where (vend_id = 1001 or vend_id = 1003) and prod_price >= 10;

这样检索的结果就是 vend_id 为 1001 或 1003 的产品里,所有 prod_price 大于等于 10 的产品列表了。

任何时候使用具有 andor 操作符的 where 子句,都应该使用圆括号明确地分组操作符。不要过分依赖默认计算次序,即使它确实是你想要的东西也是如此,而且使用圆括号能消除歧义,增加可读性。

5.5 范围操作符 in (set)

使用 in 操作符可以指定条件范围,范围中的每个条件都可以进行匹配。in 要匹配的值放在其后的圆括号中:

# 找出id为 1003 或 1001 的供应商
select * from products where vend_id in (1001, 1003);

in 操作符可以用 or 来取代,在以下情况建议使用 in

  1. 在选项比较多时,in 更清楚且更直观;

  2. 使用 in 时,计算的次序更容易管理(因为使用的操作符更少);

  3. in 一般比 or 操作符清单执行更快;

  4. in 的最大优点是可以包含其他 select 语句,使得能够更动态地建立 where 子句。

5.6 逻辑否操作符 not

not 否操作符可以和前面的 inbetween and 一起使用,表示对范围取反:

# 找出id不为 1001 1003 的产品
select * from products where vend_id not in (1001, 1003);

# 选择产品价格不在 5 到 15 之间的产品
select * from products where prod_price not between 5 and 15;

5.7 like 操作符

比如想找出名称中包含 anvil 的所有产品,可以通过 like 操作符来进行搜索。like 表示后面跟的搜索模式使用通配符匹配而不是直接相等匹配。

操作符 %

最常使用的通配符是 % 操作符,% 表示任意多个字符,包括没有字符。

# 找出产品名字以 jet 开头的产品
select * from products where prod_name like 'jet%';

# 找出产品名中含有 on 的产品
select * from products where prod_name like '%on%';

# 找出产品名以 s 开头,以 e 结束的产品
select * from products where prod_name like 's%e';

注意,% 是无法匹配 null 的。

操作符 _

_ 表示任意单个字符。

select * from products where prod_name like '_ ton anvil';

另外,转译使用 \\,比如 \\_

# 找到描述中有 % 的产品
select * from products where prod_desc like '%\\%%';

注意:

  1. 不要过度使用通配符。如果其他操作符能达到相同的目的,应该使用其他操作符。

  2. 在确实需要使用通配符时,除非绝对有必要,否则不要把它们用在搜索模式的开始处。把通配符置于搜索模式的开始处,搜索起来是最慢的。

5.8 正则表达式 regexp

关于正则表达式,可以先简单看一下 「正则表达式必知必会」[8] 这篇博客。

使用 regexp 关键字来表示匹配其后的正则表达式:

# 找到产品名以 1000 或 anvil 结尾的产品
select * from products where prod_name regexp '1000|anvil$';

正则表达式中转译使用 \\\\,比如希望查找 . 这个字符而不是正则中的 . 通配符,使用 \\\\.,为了转移 \\ 这个字符,使用 \\\\\\

# 找到产品名以 . 字符开头的产品
select * from products where prod_name regexp '^\\\\.';

6. 计算字段

有时候我们需要直接从数据库中检索出转换、计算或格式化过的数据,而不是检索出数据,然后再在客户机应用程序或报告程序中重新格式化,这时我们就需要计算字段了。

6.1 别名 as

查询出来的虚拟表格可以起一个别名,方便理解,可读性好,另外如果查询的字段有重名的情况,可以使用别名 as 来区分开来。

# 使用 as 关键字
select cust_name as name from customers;

# as 关键字也可以直接省略
select cust_name name from customers;

# 可以给不同字段分别起别名
select cust_name name, cust_city location from customers;

6.2 拼接 concat

想把多个字段连接成一个字段,可以使用到拼接字段函数 concat

# 将供应商的名字和地点拼接好后返回,并命名为 vend
select concat(vend_name, '(', vend_country, ')') vend from vendors;

注意中间如果有任何一个数据为 null,拼接的结果也是 null

所以对某些可能为 null 的字段要使用 ifnull 函数判断一下,第一个参数为要判断的字段,第二个参数是如果是 null 希望返回的结果:

# 将顾客信息拼接起来
select concat(cust_name, '(', ifnull(cust_email, '-'), ')') customerInfo from customers;

如果表中的数据前后有空格,可以使用 rtrim() 函数去除右边空格,ltrim() 去除左边空格,或者 trim() 去除前后空格:

# 将顾客信息处理后拼接起来
select concat(rtrim(vend_name), '(', trim(vend_country), ')') vend from vendors;

6.3 算术计算 +-*/

基本的算术运算符在 select 语句中也是支持的:

# 计算订单每种总额,并按照总金额降序排列
select prod_id as id, quantity, quantity*item_price as totalPrice 
from orderitems order by totalPrice desc;

基本运算符加减乘除都是支持的 +-*/

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,因为 以上是关于MySQL 从入门到实践,万字详解!的主要内容,如果未能解决你的问题,请参考以下文章

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

Nginx 从入门到实践,万字详解!

万字详解!Git 从入门到入土最佳实践 !

万字详解!Git 入门最佳实践 !

万字详解!Git 入门最佳实践

Nginx | 万字文章助你从入门到实践!