语法为 update 表名 set 字段=表达式,字段=表达式...where筛选条件order bylimit,它的执行逻辑是先依照 where 条件查询匹配的数据,再对筛选出来的行完成字段内容的更新修改。
举例:
我们继续使用之前的成绩表 exam_result。
set的使用
继续,比如我们要将孙悟空同学的数学成绩变更为80分:
我们的需求是把孙悟空的数学成绩修改为 80 分,先通过 select name,math from exam_result; 查看原始数据,能看到孙悟空初始数学分数是 78;再执行 update exam_result set math=80 where name='孙悟空';,语句依靠 where 精准定位姓名为孙悟空的单行数据,执行后返回 1 行受影响,再次查询,对应 math 字段从 78 更新为 80。这里我们要重点强调:当 update 不加 where 条件时,会把全表的所有字段都进行更新,就会导致极易误改全表数据,所以慎用。
继续,比如我们要将曹孟德同学的数学成绩变更为60分,语文成绩变更为70分:
如果我们想要单次修改同一个人的多个字段,在 set 后用英文逗号分隔多组字段 = 新值即可,比如我们要将曹孟德同学的数学成绩改成 60、语文改成 70,执行 update exam_result set math=60,chinese=70 where name='曹孟德';,这条 SQL 通过 where 锁定曹孟德单条记录,一次性完成两个不同字段的数值修改,查询数据表可以看到两条字段同步更新完毕。
这里我们需要注意 set 后面多个字段赋值之间,绝对不能用 and,只能用英文逗号 , 分隔,and 不能替换逗号,替换后会语法报错。这里的逗号是用来分隔多条并列的字段赋值表达式,代表同一行数据一次性修改多个列,属于 set 子句内部的分隔符。而 and 是逻辑运算符,只用在 where 条件里用来拼接多个筛选条件、做行匹配判断,and 的生效范围在 where 后,不能放到 set 的字段赋值之间。
继续,比如我们要将总成绩倒数前三的3位同学的数学成绩加上30分:
第一步先用查询语句定位目标数据 select name,math+chinese+english total from exam_result order by total asc limit 3;,order by total asc 按总分从小到大升序,搭配 limit 3,筛选出总分最低的宋公明、刘玄德、曹孟德三人。
MySQL 的 update 语法支持搭配 order by + limit,执行语句 update exam_result set math=math+30 order by chinese+english+math asc limit 3;。set math=math+30 代表在原有数学分数基础上加 30,MySQL 不支持 math+=30 这种简写,只能用原值参与运算写法;order by 先按总分升序排序,limit3 限定只修改排序后的前 3 行,刚好命中总分倒数三位。执行后 3 行数据被修改。
修改后再次查询 select name,math+chinese+english total from exam_result order by total limit3;,发现倒数三名变成宋公明、刘玄德、唐三藏。原因是三人数学加分 30 后总分同步上涨,原本第四名唐三藏分数低于曹孟德,顶替进倒数前三,但原始指定的 3 名学生确实已经完成加分,符合题目更新要求。
继续,比如我们要将所有同学的语文成绩更新为原来的2倍:
注意:更新全表的语句慎用!
我们需要将全表所有人语文成绩改成原来 2 倍,MySQL 中没有 chinese*=2 运算符,规范写法:update exam_result set chinese=chinese*2;。这条 SQL没有 where 条件、没有 limit 限制,会遍历整张表 7 条数据,全部把 chinese 字段乘 2 翻倍,查询前后表格可以看到全部学生语文分数变为原值两倍。
标准语法为 DELETE FROM table_name WHERE ...ORDER BY ...LIMIT ...,如果省略 where、order by 与 limit 所有限定条件,直接书写 delete from exam_result; 会清空整张数据表内部的全部数据,但数据表本身的字段结构、主键、字段约束等表定义信息会完整保留,想要连同表结构一起彻底销毁数据表不能使用 delete,需要使用 drop table 语法,这是 delete 和 drop 在功能本质上最关键的区分点。
继续,比如我们要删除孙悟空同学的考试成绩
想要精准删除指定单行数据时依靠 where 条件完成范围锁定,我们需要删除孙悟空的成绩,需要先查询再执行删除,先通过 select * from exam_result where name='孙悟空'; 查看这条数据,确认待删除数据内容无误后,再执行 delete from exam_result where name='孙悟空';,这条 sql 语句依托 name 字段的等值条件精准匹配单条记录并完成删除,执行结束后再次全表查询可以发现孙悟空对应的整条数据已经从表格中消失,实际项目开发里删除操作前使用 select 先进行数据校验是规避误删的必要步骤,能提前排查 where 条件书写错误带来的风险。
继续,比如我们要删除倒数第一的同学:
delete 语句同样可以搭配 order by 与 limit 实现按排序规则限定删除行数,我们首先借助查询语句 select name, chinese+math+english total from exam_result order by total asc limit 1;,通过总分升序排序加 limit 锁定倒数第一的学生是刘玄德,确认目标后执行 delete from exam_result order by english+math+chinese asc limit 1;,数据库会先按照总分完成全表排序,再根据 limit 的限制仅删除排序后的第一条记录,执行完毕后查表能够验证刘玄德的数据已经被成功移除。
确认原表还保留六条重复数据之后,先用 select distinct * from duplicate_table; 校验去重后的结果,这条查询可以筛选出去除重复后的三条有效记录。
接着借助 insert into no_duplicate_table select distinct * from duplicate_table; 把子查询去重后的结果批量写入新建空表,执行结束后查询 select * from no_duplicate_table;,此时新表内部就存储了去重完毕的无重复数据,完整保留了原表每条唯一的记录内容。
下面我们对这两个表进行重命名操作:
最后我们通过重命名 rename 语句完成表名替换,首先执行 rename table duplicate_table to old_duplicate_table; 把存有重复原始数据的旧表改名备份,再执行 rename table no_duplicate_table to duplicate_table; 将已经去重完毕的新表改成原来业务使用的表名。
此时再查询 select * from duplicate_table;,对外使用的表名没有变化,但底层实际已经替换成去重后的干净数据表,从使用层面实现了原表原地去重。
先执行 select * from exam_result; 查看全表数据,表里一共留存 5 条学生记录。
想要统计班级总人数可以使用 count(*),语句写成 select count(*) from exam_result;,最终统计结果为 5,代表总共 5 名学生,还能借助 as 给统计字段起别名,select count(*) 总数 from exam_result;,让查询结果的表头展示自定义名称,同时补充知识点,count (1)、count (常数) 效果和 count (*) 基本一致,同样用来统计整张表有效数据行数。
继续,比如我们要统计本次考试的数学成绩分数个数
想要统计数学成绩的有效记录数量,改用 count(字段) 写法,执行 select count(math) from exam_result;,该写法会遍历 math 字段,自动忽略字段值为 null 的数据,当前表所有学生都填写了数学分数,所以统计结果依旧是 5,和 count () 结果相同,后续如果出现 math 字段为空的记录,count (math) 就会跳过 null 行,统计数值小于 count ()。
继续,比如我们要统计不重复的数学成绩
需求变更为统计不重复的数学成绩数量,我们先单独查询 select math from exam_result; 可以发现,唐三藏和猪悟能的数学分数都是 98,存在重复数值,这里需要注意 distinct 的书写位置,错误写法 select distinct count(math) as res from exam_result; 是先统计出行数 5,再对数字 5 去重,结果仍旧是 5,无法实现去重统计;正确语法是把 distinct 写在 count 的括号内部 select count(distinct math) as res from exam_result;,先对 math 字段整体去重再统计条数,剔除一组重复 98 之后,最终有效不重复成绩一共 4 个,结果返回 4。
继续,比如我们要统计数学成绩总分
首先使用 sum 函数完成数学总分统计,执行 select sum(math) from exam_result;,sum 会对 math 字段所有有效数值累加,最终算出数学总分 454。
那我们也能顺便得出平均分:
我们也可以借助 sum 和 count 手动运算求取平均分,写法 select sum(math)/count(*) from exam_result;,用总分除以总人数得到数学平均分 90.8。
套用相同逻辑 select sum(english)/count(*) from exam_result; 能够算出英语平均分 64.2,这种手动相除的方式可以直观理解平均分底层运算逻辑。
当然我们也可以统计不及格得人数:
聚合函数也能够搭配 where 条件提前过滤数据,再做统计,用来筛选指定范围的数据量。想要统计数学不及格人数就执行 select count(*) from exam_result where math<60;,筛选后无满足条件的数据,统计结果为 0;统计英语不及格则用select count(*) from exam_result where english<60;,筛选出两条记录,最终结果是 2,where 会在聚合运算前先剔除不符合条件的行,只对剩余数据做汇总。
也有更简单的求平均分的方法:
avg 是 mysql 内置求平均值的聚合函数,替代手动 sum 除法,select avg(math) from exam_result; 直接得到数学平均分 90.8,和 sum 除法计算结果一致。
同时支持多字段相加后再求平均,select avg(english+chinese+math) 平均分 from exam_result;,先把单条记录三科分数求和,再对全部总分求取平均,算出三科整体平均分 303。
继续,比如我们要返回****> 70********分以上的数学最低分****
min、max 搭配 where 实现带筛选的极值查询,需求是查找 70 分以上数学成绩里的最低分,先通过 select math from exam_result where math>70; 筛选出全部符合条件的数学分数,再嵌套 min 聚合 select min(math) from exam_result where math>70;,最终筛选出符合条件里的最小值 73,where 优先过滤数据,聚合只作用在筛选完毕的结果集上。
补充执行顺序规则,整条 sql 的执行顺序是先通过 from 确定数据表,再用 where 筛选有效数据,最后对筛选完的数据执行聚合运算,因此聚合只能作用在经过 where 过滤后的数据集中,未经过筛选的单行明细数据无法直接和聚合结果拼接查询,这也是聚合查询不能随意混杂普通字段的核心原因。
四、分组聚合统计
group by 分组子句的核心作用是按照指定字段拆分数据组,再配合聚合函数实现分组聚合统计。
语法格式为 select 字段1,聚合函数(字段2) from 数据表 group by 分组字段;,分组会把分组字段取值相同的行归集为一组,每组最终只生成一条聚合结果,日常统计部门、岗位维度数据都会依托 group by 实现。
下面我们直接举例,这里我们已经准备好了相关的表及信息了,本次案例使用经典 scott 测试库,包含 dept 部门表、emp 员工表、salgrade 薪资等级三张业务表 :
数据导入完成后,先用 show databases; 查看全库列表,能确认 scott 已经成功创建,之后分别使用 desc 表名查看三张核心数据表的字段结构,desc emp 查看员工表字段定义,包含员工编号、姓名、岗位、薪资、奖金、部门编号等字段;desc dept 查看部门表,存储部门编号、部门名称、地点;desc salgrade 查看薪资等级表,划分等级、薪资上下限,再通过select * from 表名分别查询全量原始数据,核对 emp14 条员工、dept4 个部门、salgrade5 档薪资等级的数据和初始化脚本一致。
下面我们就可以分组查询了:
需求1 : 如何显示每个部门的平均工资和最高工资:
首先我们要对这些员工按照部门进行分组 ,使用 group by deptno 分组,以部门编号作为分组依据,数据库会自动把 deptno 数值相同的所有员工归集为同一个分组,在 select 后搭配 max(sal) 统计单组最高工资、avg(sal) 计算分组平均薪资,完整语句 select deptno,max(sal) 最高,avg(sal) 平均 from emp group by deptno;,执行后数据按 10、20、30 三个部门分成三组,40 号部门暂无员工因此不会出现在结果里,最终分别输出各组的极值与平均值。
我们再补充一下分组的性质:
使用 group by 之前要先梳理业务需求,确认统计目标是按维度拆分数据再聚合,只有需要分组汇总数据时才适合选用分组语法,单条明细查询不需要使用 group by。以 group by deptno为例,分组会依据 deptno 字段里不同的数据值完成拆分,数据表中 deptno 相同的所有行会被划入同一个分组。
这里我们需要多字段联合分组,统计每个部门、每个岗位的平均薪资与最低工资,分组语法写作 group by deptno,job,代表优先按照部门编号分组,在同一个部门内部再依据岗位二次拆分分组,逻辑上类似树形分层拆分,分组维度越精细,最终输出的数据条目就越多。 完整 sql 为select deptno,job,avg(sal) 平均,min(sal) 最低 from emp group by deptno,job;,执行后一共生成 9 组统计结果,每条数据对应单一部门下单一岗位的薪资汇总数据。
我们先执行 select deptno,avg(sal) deptavg from emp group by deptno;,依靠 group by deptno 按部门分组,avg(sal) 完成分组聚合,得到 10、20、30 三个部门各自的平均薪资数据,此时聚合计算已经全部完成。
下面我们就需要对聚合好的结果进行判断了,怎么判断? having
第二步则是借助 having 关键字对聚合之后的结果做条件过滤,完整语句 select deptno, avg(sal) deptavg from emp group by deptno having deptavg<2000;,sql 执行顺序为先分组聚合,再用 having 筛选分组结果,最终仅保留 30 号部门,其平均薪资 1566.67 满足小于 2000 的条件。having 一般搭配 group by 使用,筛选对象是分组聚合完毕的数据。
那 having 和 where 的区别是什么呢?
区分 where 和 having 的核心差异,二者虽然都是条件筛选,但执行阶段完全不同。where 在分组之前生效,用来过滤原始数据表的行数据,无法识别聚合别名、聚合结果。having在分组之后生效,筛选聚合统计结果。
这里我们将 where 和 having 的顺序进行一下替换:
因为 where 在分组之前生效,用来过滤原始数据表的明细行,无法识别聚合别名、聚合结果;因此 where deptavg< 2000 直接语法报错,deptavg 是聚合后生成的字段,原始表不存在该列。having 在分组之后生效,筛选聚合统计结果,整张表本身也可以视作是一个组,所以 having 也能筛选原始字段,比如select * from emp having ename='SMITH'; 可以正常执行,但业务规范上原始行过滤优先使用 where。
where 与 having 组合使用,需求改为剔除 SMITH 员工后,筛选岗位平均薪资低于 2000 的数据,语句 select deptno,job,avg(sal) myavg from emp where ename != 'SMITH' group by deptno,job having myavg < 2000;,执行顺序依次是:from 确定数据表 → where过滤删掉 SMITH 这条原始数据 → group by按部门+岗位分组 → avg 完成分组聚合 → having筛选聚合值小于 2000 的分组,最终输出四条符合条件的统计数据。
我们要跳出只有磁盘落地、物理真实存在的数据表才算表的固有认知,在 mysql 的逻辑设计理念里遵循一切皆表的核心思想,除了通过 create 语句创建、实际存储在磁盘中的物理数据表之外,sql 执行过程里 where 筛选生成的中间临时数据集、group by 分组聚合之后生成的结果集、select 查询最终返回的结果集合,在逻辑层面全都可以视作一张表,这些逻辑表同样拥有行列结构,能够沿用单表的 CRUD 相关语法规则去做二次筛选、聚合等操作,正是基于这个设计思路,只要熟练掌握单表的增删改查使用逻辑,后续遇到多表联查、子查询、嵌套查询等各类复杂 sql 场景时,都可以把关联查询拆解成多张逻辑单表分步处理,用统一的单表处理思路逐层拆解需求,因此哪怕后续接触多表查询,底层的查询处理逻辑也和单表保持一致,只需要理清多张逻辑表之间的关联条件,就能沿用已经掌握的单表语法完成各类复杂的数据统计与筛选工作。 所以 "MySQL一切皆表" from 是整个查询的起点,数据库先根据 from 加载数据源表,生成初始逻辑数据表;紧接着执行 on,在多表关联场景下依靠 on 的关联条件筛选两张表的匹配数据,随后执行 join 完成多表拼接,生成拼接后的中间逻辑表;where 在多表拼接结束后执行,针对明细原始行做条件过滤,剔除不满足条件的数据;经过 where 过滤后的数据进入 group by 阶段,按照指定字段拆分数据分组,同时完成分组内聚合函数运算;having 紧跟分组之后,专门对聚合生成的分组结果做筛选,过滤不符合统计阈值的分组;select 在聚合筛选完毕后执行,确定最终需要展示的查询字段、字段别名;distinct 依托 select 的结果集做去重处理,剔除整行完全重复的数据;order by 在去重结束后,按照指定字段对结果全局排序;最后 limit 截取排序后的结果,限定最终返回的数据行数。with 作为公用表表达式,会在整条 sql 正式查询逻辑运行之前提前解析生成临时表,优先级高于 from。