目录
mysql基础语法与进阶
先来看看sql全家桶
| 分类 | 全名 (英文) | 全名 (中文) | 核心含义 | 核心关键字 | 通俗解释 |
|---|---|---|---|---|---|
| DQL | Data Query Language | 数据查询语言 | 只读不写。从数据库里精准提取数据,不改变任何内容。 | SELECT, FROM, WHERE, GROUP BY, ORDER BY, LIMIT | “找东西”。就像在仓库里按清单取货,只把货拿出来,绝不碰仓库里的陈列。这是日常开发用得最多的(占 80%)。 |
| DML | Data Manipulation Language | 数据操作语言 | 增删改。操作表里的数据行,不动表结构。支持事务回滚。 | INSERT, UPDATE, DELETE | “动内容”。往仓库里放货、换货、扔货。它动的是“容器里的东西”,而不是容器本身。 |
| DDL | Data Definition Language | 数据定义语言 | 建结构。定义和管理数据库对象(库、表、索引)的骨架。执行完自动提交,不可回滚! | CREATE, ALTER, DROP, TRUNCATE | “搭架子”。建仓库、打隔断、拆墙。它决定数据怎么存,一旦执行,就像泼出去的水,收不回来! |
| DCL | Data Control Language | 数据控制语言 | 管权限。控制用户访问权限,决定谁能进库、谁能动哪张表。 | GRANT, REVOKE | “发门禁卡”。给员工分配钥匙。比如让实习生只能查表不能删表,让老板拥有所有权限。保障数据库安全。 |
| TCL | Transaction Control Language | 事务控制语言 | 保一致。管理 DML 操作的提交与回滚,确保数据要么全成功,要么全失败。 | COMMIT, ROLLBACK, SAVEPOINT | “后悔药”。转账时 A 扣钱 B 没收到?赶紧 ROLLBACK!它保证业务逻辑的原子性,防止数据写到一半崩了。 |
一、关于库的操作
A.创建库
-
创建库:
create database if not exsits 数据库名称; -
创建并指定字符集(编码)与排序:
create database if not exsits 数据库名称 charset 编码集(如utf8mb4) collate 排序(如utf8mb4_0900_ai_ci)
B.库操作
- 查看当前所有库:
show databases; - 查看当前使用库:
select database; - 查看指定库下的所有表:
show tables from 库名; - 查看创建库的信息:
show create database 数据库名; - 切换/选中库:
use 数据库名; - 修改库字符集:
alter database 数据库名 charset 字符集 collate 排序方式; - 删除库:
drop database if exsits 数据库名;
二、关于表的基础操作命令
-
创建表
create table 表名(列名 列类型 列约束 [comment], #方括号代表可选列名 列类型 列约束 [comment],列名 列类型 列约束 [comment],······列名 列类型 列约束 [comment])[表可选约束] [comment];比如use sql_study;#使用前需要先切换到需要使用的对应数据库create table student(stu_name varchar(10) comment "学生姓名",stu_sex char,stu_age tinyint unsigned,stu_height double(5,2),stu_birthday date,stu_register timestamp default CURRENT_TIMESTAMP(),stu_update timestamp default CURRENT_TIMESTAMP() on update current_timestamp) -
列类型(各种数据类型,百度一下)
1.数值类型: 整数类型:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT 浮点数类型:FLOAT、DOUBLE 定点数类型:DECIMAL2.字符串类型: 固定长度字符串:CHAR 可变长度字符串:VARCHAR 文本类型:TEXT、BLOB3.日期和时间类型: 日期类型:DATE 日期时间类型:DATETIME 时间戳类型:TIMESTAMP4.其他类型: 枚举类型:ENUM 集合类型:SET这些数据类型的选择对于数据库的存储效率和查询性能至关重要。每一种数据类型都有其独特的地方,适合解决不同的存储问题。- 修改表(中括号还是可选!)
- 添加一列(可以指定添加在x前后):
alter table 表名 add 字段名 字段类型 [first|after 字段名]; - 修改列名:
alter table 表名 change 字段名 新字段名 新类型 [first|after 字段名]; - 修改列类型:
alter table 表名 modify 字段名 新类型 [first|after 字段名]; - 删除一列:
alter table 表名 drop 字段名; - 修改表名:
alter table 表名 rename [TO] 新表名;
- 添加一列(可以指定添加在x前后):
- 删除表:
drop table [if exsits] 表名 数据表1,数据表2,······; - 清空表数据:
truncate table 表名;
三、DML相关操作
| 关键字 | 核心作用 | 暴躁哥通俗解释 | 适用场景 |
|---|---|---|---|
INSERT | 新增数据 | 往表里塞新记录。可以单条插,也可以批量插,还能从别的表“复制”数据过来。 | 用户注册、下订单、日志写入。 |
UPDATE | 修改数据 | 改表里已有的内容。必须带 WHERE,不然全表数据都会被改,直接事故现场! | 修改用户密码、更新订单状态、商品调价。 |
DELETE | 删除数据 | 一行行删数据。支持事务回滚,删完空间不释放(会有碎片)。 | 删除某个特定订单、清理过期会话。 |
SELECT | 查询数据 | 只读不写。虽然严格标准里它属于 DQL,但在 MySQL 实战和面试中,它常被归为 DML 的一部分。 | 列表展示、报表统计、关联查询。 |
A.插入数据语法(插入的是一整个行,行是mysql里的最小操作单位.)
- 为表的所有字段插入值:
insert into 表名 values (value1,value2,······); - 为指定列插入指定的值:
insert into 表名 (列名1,列名2······) values (value1,value2,······); - 日期和字符串作为值,都需要加上引号!
B.修改语法
-
修改表中所有行数据(全表修改)
#更新表中所有行的指定列数据。update table_nameset column1=value1,column2=value2,······ -
修改表中符合条件行的数据(条件修改)
update table_nameset column1=value1,column2=value2,······[where condition] -
列子
use sql_study;update student set birthday = "2006-01-13" where stu_name = "大大";update student set stu_height=stu_height+2 where stu_age = 20;#可以在原列上进行运算,没有+=这种语法
C.删除数据语法
- 删除表中所有行数据:
delete from table_name; - 删除符合条件的特定行:
delete from table_name [where condition] - 比如
delete from student where stu_height >195 and stu_name="大大大";
四、DQL查询操作(单表)
select五种情况
-
情况一:非表查询,类似于直接输出(如cpp的cout,js的console.log)
select 1;select 2/9;select version(); -
情况二:指定表
select 列名1,列名2······ from 表名;或者select 表名.列名,表名.* from 表名 #表名.*代表获取所有字段 -
情况三:查询列起名
select 列名1 as 别名,列名2,列名3 as 别名,······from 表名;或者select 列名1 别名,列名2,列名3 别名,······from 表名;as可以省略但得用空格代替,起别名的意义是简化列名, -
情况四:去重
select distinct 列名 [,列名2,列名3······] from 表名;指定列值去重复行,可以指定单列或者多列,但是distinct关键字只写一次且前置多列是综合去重,全部一样才算重。 -
情况五:查询常数
select "阿达" as corporation,列名1,列名2······ from 表名;人造了一个常数列,每一行数据都将添加,且值为“阿达”。
显示表结构:desc 表名
算数运算符
| 运算符 | 作用 | 使用方法/示例 | 划重点(必看!) |
|---|---|---|---|
+ | 加法 | SELECT 100 + 50; | 它不是字符串拼接符! 在 MySQL 里 + 只干加法的活。想拼字符串给我老老实实用 CONCAT() 函数。 |
- | 减法 | SELECT 100 - 30; | 也能当负号用,比如 SELECT -5;。 |
* | 乘法 | SELECT 6 * 7; | 优先级比加减高,先乘除后加减这道理别忘了。 |
/ | 除法 | SELECT 15 / 2; | 结果永远是浮点数! 哪怕能整除,15/2 也会给你算出 7.5000,别指望它自动取整。 |
DIV | 整数除法 | SELECT 15 DIV 2; | 这才是你要的“取商”!直接砍掉小数部分,返回 7。做分页或者算个数的时候用它。 |
% 或 MOD | 取余(求模) | SELECT 15 % 2; SELECT MOD(15, 2); | 判断奇偶数神器!num % 2 = 1 就是奇数。除数是 0 的话,结果直接返回 NULL,不会报错。 |
坑点:
- 除数为 0 的坑:
在数学里除以 0 是非法的,但在 MySQL 里,
SELECT 10 / 0;或者SELECT 10 % 0;不会报错,而是温柔地返回一个NULL。如果你拿着这个结果去参与后续计算,整个表达式全变成NULL! +号的隐式转换陷阱: 如果你写SELECT '100' + '1';,MySQL 会先把字符串转成数字再相加,结果是101。 但如果你写SELECT '100a' + 1;,MySQL 会把'100a'强行转成100(非数字部分直接丢弃),结果还是101。这种隐式转换有时候会把你坑得找不着北,涉及字符串处理时,务必先用CAST()或CONVERT()显式转好类型!- 精度问题:
除法运算
/默认会保留小数点后 4 位。如果你对精度要求极高(比如算钱),建议配合ROUND()函数或者直接用DECIMAL类型。、
比较运算符
| 运算符 | 作用 | 典型示例 | 划重点(必看!) |
|---|---|---|---|
= | 等于 | WHERE age = 25 | 它不是赋值! 在 SQL 里 = 就是判断相等。另外,绝对不能用来判断 NULL,NULL = NULL 的结果是 NULL(假),查不出数据的! |
<> 或 != | 不等于 | WHERE status <> 0 | 两个写法效果一样。同样不能用来判断 NULL,想排除空值给我用 IS NOT NULL。 |
> / < | 大于 / 小于 | WHERE salary > 5000 | 日期也能直接比!比如 '2025-01-01' > '2024-12-31',MySQL 会按时间先后给你算得清清楚楚。 |
>= / <= | 大于等于 / 小于等于 | WHERE price >= 100 | 包含边界值,别搞混了。 |
IS NULL | 判断空值 | WHERE phone IS NULL | 判断 NULL 的唯一正解! 只要字段可能为空,必须用这个,用 = NULL 你一辈子都查不到结果。 |
IS NOT NULL | 判断非空 | WHERE email IS NOT NULL | 同上,专门用来筛选有值的记录。 |
<=> | 安全等于 | WHERE col <=> NULL | 这是个冷门但好用的神器。它既能判断普通值相等,也能判断 NULL。NULL <=> NULL 返回真(1),再也不用写冗长的 IS NULL 了。 |
BETWEEN A AND B | 介于 A 和 B 之间 | WHERE age BETWEEN 18 AND 30 | 闭区间!闭区间!闭区间! 包含 18 也包含 30。而且 A 必须小于 B,写反了直接查出空结果,别怪我没提醒你。 |
IN (值列表) | 匹配列表中的任意值 | WHERE dept IN (1, 3, 5) | 相当于多个 OR 的简写。注意:IN 列表里如果有 NULL,整个表达式可能会变成 NULL,小心踩坑。 |
LIKE | 模糊匹配 | WHERE name LIKE '张%' | % 代表任意多个字符,_ 代表单个字符。千万别在 % 开头用索引,否则索引直接失效,全表扫描慢到你怀疑人生。 |
REGEXP | 正则表达式匹配 | WHERE name REGEXP '^张' | 功能最强,能搞定复杂匹配,但性能略低。简单场景用 LIKE 就够了,别动不动就上正则。 |
逻辑运算符
| 运算符 | 作用 | 典型示例 | 划重点(必看!) |
|---|---|---|---|
NOT 或 ! | 逻辑非 | WHERE NOT status = 0 | 取反神器! 经常和 IN、BETWEEN、LIKE 连用,比如 NOT IN (1,2)。注意:NOT NULL 的结果还是 NULL,别指望它能查出空值! |
AND 或 && | 逻辑与 | WHERE age > 18 AND sex = '男' | 要求所有条件同时满足! 只要有一个条件是 0(假),结果直接就是 0。如果有一个是 NULL 且另一个不是 0,结果就是 NULL。 |
OR 或 ` | ` | 逻辑或 | |
XOR | 逻辑异或 | WHERE is_vip XOR is_deleted | “二者必居其一”! 两个条件真假不同时返回 1,相同(都是真或都是假)返回 0。如果有任意一个是 NULL,结果直接变 NULL。这玩意儿在筛选“互斥状态”时贼好用! |
单行函数
单行函数是对一行中的某列进行操作的函数,返回结果是单一值!
数值函数
| 函数 | 作用 | 典型示例 | 划重点(必看!) |
|---|---|---|---|
ABS(x) | 求绝对值 | SELECT ABS(-10); → 10 | 负数变正数,正数不变。算距离、差值时必用,别自己写 IF 判断了! |
CEIL(x) / CEILING(x) | 向上取整 | SELECT CEIL(3.14); → 4 | 只要小数部分不为 0,直接进位!算分页总数、容器个数时用它,别四舍五入把东西装漏了! |
FLOOR(x) | 向下取整 | SELECT FLOOR(3.99); → 3 | 直接砍掉小数部分。算年龄、工龄这种“满多少才算”的场景,必须用 FLOOR! |
MOD(x, y) | 取余(求模) | SELECT MOD(10, 3); → 1 | 等同于 x % y。判断奇偶数(MOD(n, 2))、循环分组神器。除数为 0 时返回 NULL,不会报错! |
RAND() | 返回随机数 | SELECT RAND(); → 0.78... | 返回 0 到 1 之间的随机浮点数。想打乱查询顺序?ORDER BY RAND() 用起来! 但数据量大时慢得像蜗牛,慎用! |
ROUND(x, y) | 四舍五入 | SELECT ROUND(3.556, 2); → 3.56 | y 是保留几位小数。如果 y 是负数,比如 ROUND(123, -1),结果是 120,对整数部分四舍五入! |
TRUNCATE(x, y) | 截断数字 | SELECT TRUNCATE(3.556, 2); → 3.55 | 直接砍掉多余小数,不四舍五入! 很多新手把它和 ROUND 搞混,算钱的时候用错,账对不上别来哭! |
POW(x, y) / POWER(x, y) | 求 x 的 y 次方 | SELECT POW(2, 3); → 8 | 两个写法效果一样。算复利、指数增长时必用。 |
SQRT(x) | 求平方根 | SELECT SQRT(16); → 4 | 算标准差、距离公式时常用。如果 x 是负数,直接返回 NULL! |
CONV(x, from, to) | 进制转换 | SELECT CONV(10, 10, 2); → 1010 | 把 x 从 from 进制转成 to 进制。处理底层 ID、二进制标志位时贼好用! |
雷点:
ROUND和TRUNCATE的天壤之别: 这是面试和实际开发中最容易踩的坑!ROUND(2.5)结果是3(四舍五入),但TRUNCATE(2.5, 0)结果是2(直接截断)。涉及金额计算、精度要求高的场景,务必搞清楚业务到底要不要“四舍五入”,用错了函数,财务找你就寄了!!!RAND()的性能陷阱: 想随机抽 10 条数据?很多人写SELECT * FROM table ORDER BY RAND() LIMIT 10;。数据量少时没问题,一旦表里有几十万行,MySQL 会给每一行都算一遍随机数再排序,直接把你 CPU 跑满,查询慢到超时! 大数据量随机抽取,去查查“主键范围随机法”!- 负数取余的坑:
MOD(-10, 3)的结果是-1,而不是2!MySQL 的MOD运算结果符号跟被除数(第一个参数)保持一致。如果你需要数学上严格的正余数,记得自己加个判断或者用ABS()包一下! DIV也是数值函数: 刚才算术运算符里提过DIV,它其实是FLOOR(x / y)的快捷写法。SELECT 10 DIV 3;结果是3。做整数除法时,用DIV比写FLOOR(x / y)更简洁,代码可读性更高!
字符串函数
| 函数 | 作用 | 典型示例 | 划重点(必看!) |
|---|---|---|---|
CONCAT(s1, s2, ...) | 拼接字符串 | CONCAT('Hello', ' ', 'World') → 'Hello World' | 只要有一个参数是 NULL,结果直接变 NULL! 想忽略 NULL 继续拼?给我用 CONCAT_WS!别拿 + 号去拼,MySQL 里 + 是算数的! |
CONCAT_WS(sep, s1, s2, ...) | 带分隔符拼接 | CONCAT_WS('-', '2024', '01', '15') → '2024-01-15' | WS 代表 “With Separator”。它会自动跳过 NULL 值,不会像 CONCAT 那样全军覆没。做 CSV 导出、路径拼接时必用! |
LENGTH(s) | 返回字节长度 | LENGTH('你好') → 6 (UTF8下) | 一个中文字符在 UTF8 占 3 个字节! 别拿它去判断字符个数,不然你的截断逻辑会把汉字切成乱码! |
CHAR_LENGTH(s) | 返回字符个数 | CHAR_LENGTH('你好') → 2 | 这才是真正的“字数统计”! 不管中文英文,一个字符就算 1。做用户名长度限制、短信字数统计,必须用它! |
SUBSTRING(s, pos, len) | 截取子串 | SUBSTRING('Hello', 2, 3) → 'ell' | 起始位置 pos 是从 1 开始的! 别按编程习惯写 0,写 0 会给你返回空串。len 省略则截到末尾。 |
LEFT(s, n) / RIGHT(s, n) | 左/右侧截取 | LEFT('Hello', 2) → 'He' | 比 SUBSTRING 更直观。做手机号脱敏(如 LEFT(phone, 3))、提取文件后缀时贼好用! |
TRIM(s) / LTRIM / RTRIM | 去除空格 | TRIM(' Hello ') → 'Hello' | 默认只去空格!想去掉特定字符?写成 TRIM(BOTH 'x' FROM 'xxxHelloxxx')。数据导入前必跑一遍,否则关联查询永远对不上! |
REPLACE(s, old, new) | 替换子串 | REPLACE('Hello World', 'World', 'MySQL') → 'Hello MySQL' | 全局替换!批量修正脏数据神器。但注意:它区分大小写,'world' 和 'World' 是两码事! |
UPPER(s) / LOWER(s) | 大小写转换 | UPPER('hello') → 'HELLO' | 做不区分大小写的模糊匹配时,把字段和查询值都转成大写再比,比改 collation 排序规则更稳妥! |
INSTR(s, substr) | 查找子串位置 | INSTR('Hello', 'll') → 3 | 返回首次出现的位置(从 1 开始),找不到返回 0。配合 SUBSTRING 做动态截取(如提取 @ 后面的邮箱域名)必用! |
LPAD(s, len, pad) / RPAD | 左/右填充 | LPAD('5', 3, '0') → '005' | 补零神器!生成统一长度的订单号、编号时,比写 CASE WHEN 拼接简洁一百倍! |
REVERSE(s) | 反转字符串 | REVERSE('abc') → 'cba' | 看起来没用?做回文数判断、或者需要倒序截取后缀名时,它能让你少写一堆逻辑! |
雷点:
CONCAT的 NULL 黑洞: 这是生产环境最高频的 Bug!假设你要拼姓名:CONCAT(first_name, last_name)。如果某个人没填last_name(值为 NULL),整个结果直接变 NULL,前端展示一片空白! 修正:要么用IFNULL(last_name, '')包一下,要么直接用CONCAT_WS('', first_name, last_name),让它自动忽略 NULL!LENGTH和CHAR_LENGTH的字节陷阱: 在 UTF8 编码下,LENGTH('好')返回 3,CHAR_LENGTH('好')返回 1。 如果你用SUBSTRING(name, 1, 5)去截一个包含中文的名字,刚好切在某个汉字的第 2 个字节上,这个汉字就废了,显示出来是个问号?或者乱码! 建议:涉及中文处理,无脑用CHAR_LENGTH和SUBSTRING,MySQL 的SUBSTRING是按字符截取的,相对安全;但千万别用LENGTH的结果去控制截取长度!LIKE与INSTR的性能账: 想查包含 “ABC” 的记录?WHERE name LIKE '%ABC%'和WHERE INSTR(name, 'ABC') > 0效果一样。 但注意:只要%写在最前面,索引直接失效! 全表扫描慢到你怀疑人生。如果数据量大,赶紧上 Elasticsearch 或者全文索引,别在 SQL 里硬扛!REPLACE的连环坑:REPLACE是全局替换!如果你想把'12345'里的'23'换成'X',结果是'1X45'。但如果你不小心把'1'换成'2',原来的'2'不会被再次替换,它只扫一遍。但在复杂嵌套替换时,务必小心顺序,别把自己绕晕了!
时间函数
| 函数 | 作用 | 典型示例 | 划重点(必看!) |
|---|---|---|---|
NOW() / CURRENT_TIMESTAMP | 返回当前日期和时间 | SELECT NOW(); → '2024-05-19 22:30:00' | 在一个 SQL 语句执行期间,它只计算一次! 哪怕你写了 10 个 NOW(),结果都一模一样。主从复制时用它最安全,别乱用 SYSDATE()! |
CURDATE() / CURRENT_DATE | 返回当前日期 | SELECT CURDATE(); → '2024-05-19' | 只返回年月日,不带时分秒。建表时 DEFAULT CURDATE() 能自动填当天日期,省得你在代码里传参! |
CURTIME() / CURRENT_TIME | 返回当前时间 | SELECT CURTIME(); → '22:30:00' | 只返回时分秒。做排班系统、判断营业时间段时必用。 |
YEAR() / MONTH() / DAY() | 提取年/月/日 | YEAR('2024-05-19') → 2024 | 还有 HOUR(), MINUTE(), SECOND()。做“按月份汇总销售额”这种需求,直接 GROUP BY MONTH(create_time),比在代码里循环处理快一百倍! |
DATE_ADD(date, INTERVAL expr unit) | 日期加减运算 | DATE_ADD(NOW(), INTERVAL 7 DAY) | 万能计算器! unit 可以是 DAY, MONTH, YEAR, HOUR 等。算会员到期时间、提前 3 天提醒,全靠它。ADDDATE() 是它的别名,别被名字骗了! |
DATE_SUB(date, INTERVAL expr unit) | 日期减法运算 | DATE_SUB(CURDATE(), INTERVAL 30 DAY) | 等同于 DATE_ADD 加负数。查“最近 30 天活跃用户”时,WHERE create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY),索引还能生效! |
DATEDIFF(date1, date2) | 计算两个日期的天数差 | DATEDIFF('2024-05-19', '2024-05-01') → 18 | 只算日期部分,忽略时间! 结果是 date1 - date2。如果 date1 早于 date2,结果是负数!算用户注册天数、逾期天数时,正负号搞反了老板会骂死你! |
TIMESTAMPDIFF(unit, start, end) | 计算时间差(指定单位) | TIMESTAMPDIFF(MONTH, '2024-01-01', '2024-05-19') → 4 | 比 DATEDIFF 更灵活!不仅能算天,还能直接算相差多少个月、多少年、多少小时。算年龄、算工龄,无脑用这个! |
DATE_FORMAT(date, format) | 日期转字符串 | DATE_FORMAT(NOW(), '%Y年%m月%d日') | 格式化输出神器! %Y 是四位年份,%m 是月份,%d 是日期,%H 是 24 小时制。做报表标题、前端展示格式,别在 Java/Python 里转,数据库直接给结果! |
STR_TO_DATE(str, format) | 字符串转日期 | STR_TO_DATE('2024-05-19', '%Y-%m-%d') | DATE_FORMAT 的逆操作。导入 Excel 脏数据时,文本格式的 '2024/05/19' 必须用它转成日期类型才能存进 DATETIME 字段! |
UNIX_TIMESTAMP() / FROM_UNIXTIME() | Unix 时间戳互转 | UNIX_TIMESTAMP(NOW()) → 1716129000 | 后端开发最爱!存数据库用 DATETIME 方便人看,传给前端用时间戳方便计算。这两个函数就是桥梁,跨平台对接时必用! |
雷点:
NOW()和SYSDATE()的致命区别: 很多人以为它俩一样,大错特错!NOW()在语句开始执行时就定死了,整个查询过程中值不变;而SYSDATE()是动态的,每调用一次就取一次系统时间。 建议:在主从复制、存储过程或者涉及SLEEP()的场景下,无脑用NOW()!用SYSDATE()会导致主库和从库记录的时间不一致,数据对不上账别来哭!DATEDIFF的正负号陷阱:DATEDIFF('2024-05-01', '2024-05-19')的结果是 -18!它是用第一个参数减去第二个参数。 如果你想算“距离到期还有几天”,一定要把未来时间放在前面,或者外面包个ABS()取绝对值。写反了导致给用户发“逾期 -3 天”的短信,场面会非常尴尬!DATE_FORMAT的大小写敏感:%m是月份(01-12),%i才是分钟(00-59)!很多新手想拼HH:MM:SS,结果写成%H:%m:%s,把月份当成分钟显示出来了。 记住口诀:分钟是i(minute 中间那个 i),秒是小写s,毫秒是大写f。格式符写错了,MySQL 不会报错,但会给出一堆莫名其妙的数字!- 时间运算的索引失效:
想查“2024 年的所有订单”?千万别写
WHERE YEAR(create_time) = 2024!这会让create_time字段上的索引直接失效,全表扫描慢到爆炸! 正确写法:WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'。用范围查询代替函数计算,索引才能乖乖干活!
流程控制
函数
| 维度 | IF() 函数 | IFNULL() 函数 |
|---|---|---|
| 本质 | 三元运算符(条件判断) | 空值替换专家(兜底处理) |
| 参数个数 | 3个:IF(条件, 真值, 假值) | 2个:IFNULL(表达式, 默认值) |
| 判断逻辑 | 判断第一个参数是否为 TRUE(非0且非NULL) | 只判断第一个参数 是否为 NULL |
| 典型场景 | 成绩分级、状态转换、二选一逻辑 | 防止计算结果为 NULL、给空字段设默认值 |
| 等价写法 | 类似 Java/JS 的 condition ? v1 : v2 | 类似 Java 的 Optional.orElse(default) |
语句:下面前两个最重要
| 分类 | 语法结构 | 典型示例 | 划重点(必看!) |
|---|---|---|---|
| 分支结构 | IF ... THEN ... END IF; | IF val IS NULL THEN SET res = 0; ELSE SET res = val; END IF; | 这是语句!不是函数! 必须写在 BEGIN...END 块里。支持 ELSEIF 和 ELSE。别把它跟单行函数 IF(expr, v1, v2) 搞混了,俩东西完全不是一回事! |
| 分支结构 | CASE ... WHEN ... END CASE; | CASE val WHEN 1 THEN SET res='A'; WHEN 2 THEN SET res='B'; ELSE SET res='C'; END CASE; | 类似 Java 的 switch。还有另一种写法是 CASE WHEN val > 10 THEN ...,适合判断范围。注意: 放在 BEGIN...END 里末尾必须加 CASE,跟在 SELECT 后面就不用加! |
| 循环结构 | WHILE ... DO ... END WHILE; | WHILE i <= n DO SET sum = sum + i; SET i = i + 1; END WHILE; | 先判断,后执行! 如果初始条件就不满足,循环体一次都不会跑。记得在循环体里更新变量(比如 SET i = i + 1),不然就是死循环,直接把你 CPU 跑满! |
| 循环结构 | REPEAT ... UNTIL ... END REPEAT; | REPEAT SET i = i + 1; UNTIL i > 10 END REPEAT; | 先执行,后判断! 哪怕条件一开始就成立,它也会至少执行一次。注意 UNTIL 后面跟的是退出条件(满足了就停),跟 WHILE 的逻辑正好相反,别写反了! |
| 循环结构 | LOOP ... END LOOP; | add_loop: LOOP IF i > 10 THEN LEAVE add_loop; END IF; SET i = i + 1; END LOOP add_loop; | 裸循环! 它自己没条件,不写退出逻辑就是死循环。必须配合 LEAVE 使用。适合逻辑复杂、需要在循环中间退出的场景。 |
| 跳转语句 | LEAVE label; | LEAVE add_loop; | 相当于 break! 用来退出指定标签的循环。必须在循环前加标签(如 add_loop:),不然 MySQL 不知道你要跳出哪一层! |
| 跳转语句 | ITERATE label; | ITERATE add_loop; | 相当于 continue! 跳过本次循环剩下的语句,直接进入下一次循环。同样必须带标签,别漏了! |
聚合(多行)函数
| 函数 | 作用 | 典型示例 | 划重点(必看!) |
|---|---|---|---|
COUNT() | 统计行数 | SELECT COUNT(*) FROM emp; | 统计总行数神器! COUNT(*) 包含 NULL 值,只要这行有数据就计数;COUNT(字段) 会忽略 NULL!想算“有提成的人数”?用 COUNT(comm),别傻乎乎去加 WHERE comm IS NOT NULL! |
SUM() | 求总和 | SELECT SUM(sal) FROM emp; | 只认数字! 如果你拿它去加字符型字段(比如 SUM(name)),MySQL 不会报错,但会返回 0 并甩给你一堆警告。算钱的时候务必确认字段类型是 DECIMAL 或 INT,不然账对不上别来哭! |
AVG() | 求平均值 | SELECT AVG(sal) FROM emp; | 自动踢掉 NULL! 计算时,分母是“非 NULL 的行数”,不是总行数。如果 10 个人里有 3 个工资是 NULL,AVG 只会除以 7。想包含 NULL(当成 0 算)?先写 AVG(IFNULL(sal, 0))! |
MAX() | 求最大值 | SELECT MAX(hiredate) FROM emp; | 全能选手! 不仅能比数字,还能比日期(找出最晚入职)、比字符串(按 ASCII 码找出字典序最大的名字)。同样忽略 NULL,别担心空值干扰结果。 |
MIN() | 求最小值 | SELECT MIN(sal) FROM emp; | 同上,找最早入职、最低工资、字典序最小的名字全靠它。 |
雷点:
1. WHERE 子句里严禁出现聚合函数!
这是新手最容易犯的语法错误!你想查“工资高于平均工资的员工”,千万别这么写:
-- ❌ 绝对报错!ERROR 1111: Invalid use of group functionSELECT ename, sal FROM emp WHERE sal > AVG(sal);为什么? 因为 SQL 的执行顺序是 FROM -> WHERE -> GROUP BY -> SELECT。执行到 WHERE 的时候,数据还没分组呢,AVG(sal) 根本算不出来!
✅ 修正: 这种需求必须用子查询或者 HAVING!
-- ✅ 正确写法:用子查询先算出平均值SELECT ename, sal FROM emp WHERE sal > (SELECT AVG(sal) FROM emp);2. COUNT(*)、COUNT(1) 和 COUNT(字段) 到底选哪个?
COUNT(\*):MySQL 官方优化过,统计总行数最快!不管字段是不是 NULL,只要是一行有效记录就 +1。日常统计总数,无脑用它!COUNT(字段):只统计该字段不为 NULL 的行数。比如COUNT(comm)能直接告诉你“有多少人拿了提成”。COUNT(1):效果和COUNT(*)几乎一样,但在老版本 MySQL 或某些特定引擎下,COUNT(*)的优化更好。别听网上瞎扯什么COUNT(1)更快,现代 MySQL 里它俩没区别,写COUNT(\*)语义更清晰!
3. 聚合函数不能嵌套!
别想着偷懒写 SELECT AVG(SUM(sal)) FROM emp GROUP BY deptno;,MySQL 直接报错。聚合函数只能套一层,想搞复杂统计,乖乖用子查询包起来!
4. HAVING 才是聚合函数的归宿
如果你想对分组后的统计结果做筛选(比如“找出平均工资大于 2000 的部门”),必须用 HAVING,别用 WHERE!
-- ✅ 正确写法:HAVING 在分组后过滤,能用聚合函数SELECT deptno, AVG(sal) FROM emp GROUP BY deptno HAVING AVG(sal) > 2000;记住口诀:WHERE 过滤原始行,HAVING 过滤统计结果。搞混了查出来的数据就是错的!
五、高级查询
1.分组查询
概念:先将数据行按某一或者多种特性进行分组,最后查询每组特性,分组查询的结果只能是分组特性列或者聚合函数。
公式:select 分组列,分组列,聚合函数 from table [where condition] [group by 分组列,分组列······ having 分组后条件]
注意这里的having,他是分组后的条件,跟where不同,where是分组前就按条件进行了。
并且他有个特性是where没有的!就是它能使用别名,这个我们稍后就会讲到。
示例:select gender,count(*),AVG(salary) as av from emp group by gender having av>11000;2.排序查询
概念:按照某一或者多特性列进行排序,不会影响结果条数,只改变顺序
公式:select 列,列 函数 from table [where condition] [group by 排序列 asc|desc ······]
asc为正序是默认的,desc是倒叙。如果是多列排序,只有第一列相同,第二列才会生效。
示例select * from emp where commission_pct is not null order by salary desc,birthday desc;3.数据分割(分页查询)
概念:将结果进行分页切割,按照特定区域展示。
公式:select 列,列,函数 from table [where condition] [limit 偏移量,行数]
偏移量表示跳过多少条数据,默认是0.行数表示要返回的是多少条数据。
示例select * from emp where gender = "女" order by salary desc limit 5,1;意思是查询工资第六高的女生。limit常常用于分页查询,公式如下limit (page-1)*size,size;4.select执行流程(相当重要)
我们书写的顺序是
select 列名 from ··· where ··· group by ···having···order by··· limit···语句执行顺序则是
from 数据源 where 筛选前置条件 group by 分组 having 分组后过滤 select 列名 order by 顺序 limit 切割所以where不能用别名而orderby可以,因为在select查询的时候起了。
六、约束
约束概念:表级别的约束,数据的限制语法。
约束作用:确保表数据的准确性、可靠性、正确性。
添加时机:1.创建表时直接添加(create table)。2.创建表之后修改(alter table)
| 完整性类型 | 核心作用 | 对应约束关键字 | 暴躁哥 |
|---|---|---|---|
| 域完整性 | 保证单元格数据的正确性 | NOT NULL, DEFAULT, CHECK | 管“填得对不对”。比如年龄不能填负数、性别只能填男或女、姓名不能为空。它针对的是单个字段的取值范围。 |
| 实体完整性 | 保证每一行记录的唯一性 | PRIMARY KEY, UNIQUE, AUTO_INCREMENT | 管“是不是同一个人”。比如身份证号不能重复、学号不能为空。它确保表里每一行数据都能被唯一识别,没有两条完全一样的记录。 |
| 参照完整性 | 保证多表之间关联的正确性 | FOREIGN KEY | 管“关系乱没乱”。比如订单表里的 user_id 必须在用户表里真实存在。它防止出现“孤儿数据”,确保表与表之间的引用关系不崩塌。 |
1. 域完整性:单元格的“质检员”
域完整性(Domain Integrity)关注的是列中每个单元格的数据是否符合特定的数据类型和范围。如果域完整性没做好,你的数据库里就会出现“年龄 = -5”、“性别 = ‘外星人’”这种离谱数据。
NOT NULL:强制必填。比如注册时的用户名,绝对不能是NULL,否则系统都不知道在叫谁。DEFAULT:兜底值。比如用户注册没选头像,自动给个默认图,避免前端展示时裂开。CHECK:范围限制。比如CHECK (age >= 18),直接拦住未成年人的注册请求。注意: MySQL 8.0.16 之前版本写了CHECK也不生效,老版本得靠应用层代码校验。(这个很浪费性能)
2. 实体完整性:每一行的“身份证”
实体完整性(Entity Integrity)的核心是主键约束。它要求表中每一行数据必须是唯一的,且能被精准定位。
PRIMARY KEY:非空且唯一。一张表只能有一个主键,它就像人的身份证号,决定了这条记录的身份。UNIQUE:唯一但允许为空。比如用户的邮箱或手机号,可以暂时不填(允许NULL),但一旦填了就不能和别人重复。AUTO_INCREMENT:自增 ID。通常配合主键使用,让数据库自动生成不重复的 ID,省得你去代码里算下一个 ID 是多少。
3. 参照完整性:多表关联的“红线”
参照完整性(Referential Integrity)是多表级的约束,全靠外键(Foreign Key)来维持。它规定了子表中的外键值,必须存在于父表的主键或唯一键中。
- 防孤儿数据:比如
orders表里有个customer_id,如果customers表里根本没有 ID 为 9527 的客户,那么订单表里绝不允许插入customer_id = 9527的记录。 - 联动删除/更新:外键最精髓的是
ON DELETE CASCADE。比如删掉某个客户时,他名下的所有订单是跟着一起删(CASCADE),还是把订单的客户 ID 设为NULL(SET NULL),或者直接禁止删除(RESTRICT)。这全靠在定义外键时指定行为逻辑,不用你在代码里写一堆DELETE语句去擦屁股。 - 硬性前提:想用外键,两张表的存储引擎必须都是 InnoDB,MyISAM 引擎根本不支持外键!而且外键列和引用的主键列,数据类型必须完全一致。
别把 NULL 当空字符串:在域完整性里,NOT NULL 约束的是 NULL 值。但 NULL 不等于空字符串 '',也不等于 0!NULL 代表“未知”。如果你给字段设了 NOT NULL 但没给 DEFAULT,插入数据时省略该字段,MySQL 会直接报错 Field doesn't have a default value。外键不是越多越好:虽然参照完整性听起来很美好,但在高并发互联网项目中,很多架构师会故意禁用物理外键。因为外键约束会在每次插入、删除时触发数据库层面的检查,严重拖慢写入速度,还容易导致死锁。暴躁建议:如果是内部管理系统、金融核心系统,数据一致性大于天,必须开外键!如果是日活千万的社交APP,建议在代码层(Service层)做逻辑校验,数据库只留主键和唯一索引,把性能榨干!CHECK 的版本坑:如果你还在用 MySQL 5.7,别指望 CHECK (age > 0) 能拦住负数!它解析语法但直接忽略执行。这种情况下,要么升级数据库,要么老老实实在 Java/Python 代码里做参数校验,别把希望全寄托在数据库上。域级(列)约束
1.非空约束
添加(建表时)create table 表名称( 字段名 类型 Not NULL 字段名 类型 Not NULL)
建表后修改alter table 表名 modify 字段名 数据类型 not null;
删除alter table 表名 modify 字段名 数据类型 null或者alter table 表名 modify 字段名 数据类型不加默认允许null。2.默认值约束
建表添加create table 表名称( 字段名 类型 default 默认值, 字段名 类型 Not NULL default 默认值)
建表后修改alter table 表名称 modify 字段名 类型 default 默认值;#如果这个字段原来就有非空约束,你还保留非空约束,那么你在修改加默认值时还必须保留非空约束,不然就没了alter table 表名称 modify 字段名 类型 default 默认值 not null;
删除alter table 表名称 modify 字段名 数据类型;#删除默认值约束,也不保留非空约束
alter table 表名称 modify 字段名 数据类型 not null;#删除默认值约束,保留非空约束3.检查约束
添加(建表时)create table 表名称( 字段名 类型 Not NULL, check(表达式),#check约束属于表级别,不用添加到列后。 字段名 类型 Not NULL default 默认值);
建表后修改alter table 表名 add constraint 约束名 check(表达式);#约束名不能重复
删除alter table 表名 drop constraint 约束名;查看约束的方法——百度一下都知道😋
实体(表)级约束
1.唯一约束
添加(建表时)create table 表名称( 字段名 类型 unique, 字段名 类型 unique key);create table 表名称( 字段名 类型 , [constraint 约束名] unique key(字段名));建表后修改alter table 表名 add constraint 约束名 unique(列名,列名······)2.主键约束
主键作用:确保行数据至少有一列是不重复的,避免了数据整行重复,我们可以把永远不重复且非null定为主键列。如学生号,id。
自定义主键:人为创建的一列,专门用来做标记不重复。
自然主键:实体自带的属性列,并且唯一不为空。比如dna序列。
细节说明主键数量:每个表只能有一个主键单一和复合:主键可以由单个列或多个列构成(复合主键)主键列类型:可以是任意的,只要不重复。主键命名:一半采用xx——id。主键索引:创建主键约束时,系统默认在列或列组合上建立对应的主键索引(根据主键查询效率高)。如果删除主键约束了,对应索引也会被删除,主键索引命令primary主键约束
光有主键只是标识了对象,但对这个对象的要求(不为空,不重复)并没有实现。所以我们必须还要实现主键约束。
语法添加(建表时)create table 表名称( 字段名 类型 primary key,#列级模式);create table 表名称( 字段名 类型 , [constraint 约束名] primary key(字段名),#表级模式);建表后修改alter table 表名 add primary key(字段列表)
#删除alter table 表名称 drop primary key;3.自增长约束
作用:限定某个列插入数据不需要维护,值自动增长。
特点:只能加在主键列,普通的不行。每张表只能有一个。增加自增长的必须是整数类型。如果设置0或者null,他会自己增长。
语法添加(建表时)create table 表名称( 字段名 类型 primary key auto_increment,#列级模式);create table 表名称( 字段名 类型 unique key auto_increment,#表级模式);建表后修改alter table 表名 modify 字段名 数据类 auto_increment
#删除alter table 表名称 modify 字段名 类型多表(外键)约束
实际开发不常用,注意是外键约束不常用!!!比较耗费性能!
外键:学生表里有个sid,分数表里有个sid,所以分数id参照引用学生里的sid。所以指的是引用或参照其他表的主键列的值,我们称之为外键,外键值因该正确引用主键!所以有了约束。
外键约束:确保外键正确引用主键。
细节数量:一个表可以有多个外键外键跨表:外键是跨表引用其他表的主键,被引用为主表,外键表为子表。外键类型:外键类型不能是任意类型,必须和逐渐类型对应且命名尽量相同主外键关系:关系型数据给中的关系就是指的主外键关系,有了关系即可联查。其他影响:存在外键约束,删除主表数据时可能会因为子表引用而删除失败,所以必须先删子表。语法添加(建表时)create table 主表名称( 字段名 类型 primary key);#子表中添加主外键约束create table 子表名称( 字段名 类型 primary key, [constraint 外键约束名称] foreign key(外键) references 主表名(主键) [on update xx][on delete xx]);建表后修改alter table 表名 add [constraint 约束名] foreign key(从表的字段) references 主表名(被引用字段)[on update xx][on delete xx]
#删除(1)先查看约束名和删除外键约束select *from information_schema.table_constraints where table_name ="表名称";alter table 从表名 drop foreign key 外键名;(2)查看索引名和删除索引,只能手动删show index from 表名称;alter table 从表明 drop index 索引名;约束等级设计([on update xx]里面的xx可填值)
| 选项值 | 中文释义 | 行为描述(父表变动时,子表咋办?) | 适用场景 | 划重点(避坑指南) |
|---|---|---|---|---|
CASCADE | 级联 | 父删子也删,父改子也改。 父表记录删除/更新,子表所有关联记录自动跟着删除/更新。 | 强依赖关系。 比如:删除了“订单”,那“订单明细”留着也没用,必须一起删。 | 最危险! 手一抖删了父表一条数据,子表几万条关联数据瞬间灰飞烟灭!生产环境慎用,容易引发“数据雪崩”。 |
SET NULL | 设空 | 父删子变空,父改子变空。 父表记录删除/更新,子表外键字段自动设为 NULL。 | 弱依赖关系。 比如:员工所属的“部门”撤销了,但员工档案要保留,只是把部门字段清空。 | 硬性前提:子表的外键字段必须允许为 NULL!如果你加了 NOT NULL 约束,执行时会直接报错。 |
RESTRICT | 限制 | 你敢动,我就报错。 只要子表有记录引用了父表,父表就禁止删除或更新该记录。 | 核心数据保护。 比如:只要有订单存在,就禁止删除对应的“客户”,防止产生孤儿订单。 | MySQL 默认值!如果你不写 ON DELETE/UPDATE,MySQL 默认就是 RESTRICT。这是最安全的选择。 |
NO ACTION | 无操作 | 效果同 RESTRICT。 检查到有关联记录就拒绝操作,回滚事务。 | 同 RESTRICT。 | 在 MySQL 的 InnoDB 引擎里,NO ACTION 和 RESTRICT 完全等价。写哪个都行,别纠结。 |
SET DEFAULT | 设默认值 | 父表变动时,子表外键设为默认值。 | 极少使用。 | 大坑预警:MySQL 的 InnoDB 引擎根本不支持这个选项!写了会报错或者被忽略。别在 MySQL 里用这个! |
最好采用on update cascade on delete restrict
七、多表关系
表和表之间可以建立主外键来连接。
分类:
- 一对一:两个表之间的每行数据都是唯一的对应关系。比如一个员工与其唯一的员工档案
- 一对多:一个表中关联另一个表多行数据,反方向只关联一行数据。比如一个作者和多个文章。
- 多对多:两个表里都可以与对方多个记录相关联。比如学生与课程。
ps:多对多的表必须额外再创建一个中间表!因为外键不可能就靠两个表完成,什么意思?无论是学生表还是课程表,只加一处外键都无法满足多对多。试想一下,我们在学生表里添加一列外键,那么每个学生只能关联上一个课程。
一对一
一对一并不能解决数据冗余的问题,它存在的意义是一张存放冷数据,一张存放热数据,那么这样查询起来就会快一些。
示例#创建主表create table emp( e_id int primary key auto_increment, e_name varchar(20) not null, e_age int default 18, e_gender char default “男”)方式一:新建一个外键create table 子表( e_id int primary key auto_increment, p_address varchar(100) not null, p_level int default 10, e_id int unique,#外键唯一 constraint s_p_1 foreign key(e_id) references emp(e_id))方式二:共用主键create table profile2( e_id int primary key auto_increment, p_address varchar(100) not null, p_level int default 10, constraint s_p_2 foreign key (e_id) references emp(e_id))一对多
一对多的特点:主表对应多条子表数据,子表对应主表至多一条数据。它可以解决数据冗余的问题,创建方式就是正常添加外键即可。
主表:作者表create table author( a_id int primary key auto_increment, a_name varchar(20) not null, a_age int default 18, a_gender char default "男")子表:文章表(外键)create table blog( b_id int primary key auto_increment, b_title varchar(100) not null, b_content varchar(600) not null, a_id int,#外键 constraint a_b_fk foreign key(a_id) references author(a_id))多对多
有一个中间表用于间接关联。
主表:学生表create table student( s_id int primary auto_increment, s_name varchar(20) not null, s_age int default 18)中间表:student_coursecreate table student_course( s_id int primary key auto_increment, s_id int, c_id int, foreign key(s_id) references student(s_id), foreign key(c_id) references course(c_id))课程表:coursecreate table student_course( c_id int primary key auto_increment, c_name varchar(10) not null, c_teacher varchar(10))八、多表查询
多表查询分为两种:一是垂直合并语法(上下拼接),另一种就是水平合并(需要主外键关联)。
垂直合并
语法#情况一:不去重(性能好)select aid,aname from a;union;select bid,bname from b;
#情况二:去重select aid,aname from a;union all;select bid,bname from b;注意:要合并的列必须是相同数量并且类型相同!
水平查询
核心要求:两个表必须要有关系(主外键相等)
拆表产物:由于拆表存储,所以语法重要
内连接和外连接的根本区别,就在于“要不要保全一方”。内连接是“铁面无私”,只留两边都有的;外连接是“偏心护短”,哪怕另一边没匹配上,也要把主表的数据全留下来!
1.内连接
内连接是一种用于从两个或多个表中检索数据的查询方式。内连接会根据指定的连接条件,将两个表中满足条件的行进行匹配,返回成功的行。
并且内连接必须满足要有主外键!什么意思?假如一个员工没有部门,且部门是外键,它的为null。那么他的数据就不会被查到。
内连接: from 表1 [inner] join 表2 on 表1.主键=表2.外键(标准)或者from 表1,表2 where 表1.主键=表2.外键(非标准)#还可以在select 后加上as起别名。2.外连接
与内连接不同,外连接会返回所有符合条件的行,同时还会返回未匹配的行。分为左外连接,右外连接。也就是通过左和右来指定一个逻辑主表,逻辑主表的数据一定会查询到。
语法:select * from 表1 left [outer] join 表2 on 表1.主键=表2.外键(左外)select * from 表1 right [outer] join 表2 on 表1.主键=表2.外键(右外)一但选择左或者有右,那后序都是左,右。
| 维度 | 内连接 (INNER JOIN) | 外连接 (OUTER JOIN) |
|---|---|---|
| 核心逻辑 | 求交集。只返回两张表中完全匹配的行。 | 求并集 + 交集。以某张表为基准,保留其所有行,另一张表匹配不上的填 NULL。 |
| 数据去向 | 不匹配的直接丢弃!如果员工没部门,或者课程没人选,这些记录在结果里直接消失。 | 强制保留主表数据。即使右表没有对应记录,左表的行依然会显示,只是右表字段变 NULL。 |
| 关键字 | INNER JOIN(INNER 可省略,直接写 JOIN 默认就是内连接)。 | LEFT [OUTER] JOIN、RIGHT [OUTER] JOIN、FULL [OUTER] JOIN。 |
| 适用场景 | 查“有成绩的学生”、“下了单的客户”。只要两者关联上了才需要看。 | 查“所有学生及其成绩”(包括没考试的)、“所有部门及其员工”(包括空部门)。 |
3.自然连接
自然连接是外连接和内连接的升级版,会自动找到两个表中相同的列名判定相等,所以可忽略 on主=外。
语法select * from emp natural join dept;#自然内连接select * from emp natural left join dept;#自然左外连接select * from emp natural right join dept;#自然右外连接4.自连接
自连接并不是新的语法,是指一个表与自身连接的操作。他在查询中使用相同表的别名来表示两个不同的实例,然后通过连接条件将这两个实例连接。
比如一张表里本来就存在引用关系,要查询关联在同一张表的不同行。
有一张员工表,领导也算在内!查询员工的编号,姓名,领导编号——》单表查询查询员工的编号,姓名,领导编号,领导姓名——》自连接,一个表用两次语法select e1.eid, e1.ename ,e1.mid, e3.enmae from t_emp e1 left join t_emp e2 on e1.mid=e2.eid lefr join t_emp e3 on e2.mid = e3.mid where e1.eid = 5子查询
子查询俗话说就是嵌套select的使用,它可以套dml操作。查到的情况有如下
1.单行单列
查询每个部门的平均工资和公司的平均工资之差。#1.查询公司的平均工资select avg(salary) from t_emp;#2.分组查询部门工资select did,avg(salary) from t_emp group by did;#3.select did,avg(salary),avg(salary)-(select avg(salary) from t_emp) group by did2.单行多列
select * from t_emp where(gender.did) in(select gender.did from t_emp where ename ="巴黎")3.多行单列
通常使用关键字:in,not in ,all(相当于and,要满足全部),any(相当于or,满足一个即可
select * from t_emp where did=any(select aid from t_emp where ename in ("jian","jito"))4.多行多列
一般用作一个虚拟表
select t_emp.did,dname from t_emp left join (select did,avg(salary) from emp group by did) temp on t_emp.did =temp.did#由于用作虚拟表,所以要起别名!这里是temp!5.update
#当update的表和子查询的表是同一个表时,会出现引用相同结果导致锁不让修改值的问题。需要将子查询的结果用临时表来表示。即再嵌套一层查询
update t_emp set salary = (select salary from (select salary from t_emp where ename ="sun") temp ) where ename ="jito"#这里别忘了起别名6.insert
#创键表tmp,复制某个表结构t_empcreate table tmp like t_emp#使用insert复制数据,此时insert不用写valuesinsert into tmp (select * from t_emp)#同时复制表结构和表数据,直接完成上面两个操作create table tmp as (select * from t_emp)7.delete
delete from t_emp where did = (select did from (selec did from t_emp where ename = 'joe') temp);九、数据库高级和特性
1.事务
事务(Transaction)就是数据库给你的“后悔药”和“安全锁”。它把一组 SQL 语句打包成一个不可分割的整体,要么全部成功,要么全部失败回滚,绝不允许出现“做了一半”的中间状态。
四大特性ACID
| 特性 | 全名 | 核心含义 | 通俗解释 | 底层怎么实现的? |
|---|---|---|---|---|
| A | Atomicity (原子性) | 要么全成,要么全废。事务里的操作是一个整体,不存在“部分成功”。 | 就像原子不可分割。转账时,A扣钱和B加钱必须绑定。如果A扣了钱,B还没加钱系统崩了,事务会触发回滚,A的钱自动退回去。 | Undo Log (回滚日志)。执行前先把“反向操作”记下来,一旦失败,照着日志逆向还原。 |
| C | Consistency (一致性) | 数据始终合法。事务前后,数据库的完整性约束(主键、外键、余额非负等)不被破坏。 | 这是最终目标。转账前两人总共有 2000 块,转账后还得是 2000 块,钱不能凭空消失或变多。 | 靠 A、I、D 共同保障,加上业务逻辑校验(如 CHECK 约束)。 |
| I | Isolation (隔离性) | 并发互不干扰。多个事务同时跑,彼此隔离,不能读到别人的“中间态”数据。 | 你在 ATM 取钱查余额时,别人不能同时把你的钱转走导致你查到错误金额。防止脏读、不可重复读、幻读。 | 锁机制 + MVCC (多版本并发控制)。让读写不互相阻塞,还能保证数据可见性。 |
| D | Durability (持久性) | 提交即永久。事务一旦 COMMIT,修改就永久生效。哪怕下一秒机房断电、硬盘炸了,数据也不会丢。 | 落袋为安。只要数据库告诉你“提交成功”,这数据就刻在磁盘上了,重启也能恢复。 | Redo Log (重做日志)。采用 WAL (Write-Ahead Logging) 技术,先写日志再刷盘,宕机后靠日志恢复。 |
事务实现方案1——手动提交
mysql默认自动提交事务,每一条语句都是一个事务,报错失败就回滚。我们想要把多条语句加到一个事务中,我们可以手动来。
#开启手动提交事务(取消自动提交事务)set autocommit = false;或autocommit = 0上面的语句执行后,他之后的所有sql都需要手动提交才能生效,知道恢复提交模式,#恢复自动提交set autocommit = true;或autocommit = 1#查看是否自动提交show variables like 'autocommit'例如
set autocommit =false;#设置手动提交模式update t_emp set salary = 15000 where ename = 'joy'commit#提交或者rollback#回滚。2.实现方案2——独立事务空间
start transaction;#添加多个sql命令update t_emp set salary =0 where name = “啊发发”#回滚或提交,回滚或提交之前,上面的多个sql都不会生效!commit 或者 rollback;注意!DDL不支持事务!所以回滚不了,删库跑路不能复原。
3.隔离级别
先来看看三大异常
| 异常名称 | 核心含义 | 通俗解释 | 发生场景 |
|---|---|---|---|
| 脏读 (Dirty Read) | 读到了未提交的数据。 | 别人改了一半还没保存,你就拿来用了。结果别人反悔回滚了,你手里的数据直接变废纸! | 事务 A 改了数据没提交,事务 B 读到了这个修改。 |
| 不可重复读 | 同一个事务内,两次读同一行,结果不一样。 | 你第一次查余额是 100 块,转头再查变成 90 块了。因为中间有个事务把数据改了并提交了。 | 事务 A 两次查询之间,事务 B 更新并提交了该行数据。 |
| 幻读 (Phantom Read) | 同一个事务内,两次范围查询,行数不一样。 | 你查“年龄小于 30 岁”的人有 10 个,转头再查变成 11 个了。因为中间有人插入了一条新记录。 | 事务 A 两次查询之间,事务 B 插入并提交了符合查询条件的新行。 |
核心区分:不可重复读针对的是某一行数据的值被修改(UPDATE);幻读针对的是结果集的行数发生变化(INSERT/DELETE)。
再来看看隔离级别:级别高越安全但是并发越拉
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 点评 |
|---|---|---|---|---|
| READ UNCOMMITTED (读未提交) | ❌ 可能 | ❌ 可能 | ❌ 可能 | 几乎不做隔离。性能最好,但数据一致性最差,生产环境严禁使用! |
| READ COMMITTED (读已提交,RC) | ✅ 避免 | ❌ 可能 | ❌ 可能 | 每次查询都生成新快照。只读已提交的数据,避免了脏读。Oracle 默认级别,但 MySQL 下容易遇到不可重复读。 |
| REPEATABLE READ (可重复读,RR) | ✅ 避免 | ✅ 避免 | ✅ 基本避免 | MySQL InnoDB 默认级别。整个事务复用同一个快照,保证多次读取结果一致。InnoDB 靠 MVCC + Next-Key Lock 解决了绝大部分幻读。 |
| SERIALIZABLE (串行化) | ✅ 避免 | ✅ 避免 | ✅ 避免 | 强制事务排队执行。完全摒弃并发,性能极差。除非是极度敏感的金融账务,否则别用! |
修改隔离级别,通常推荐第二个select transaction_isolation="隔离级别";查看隔离级别select @@transaction_isolation;2.用户权限管理
| 权限大类 | 核心权限 | 作用 | 适用角色 |
|---|---|---|---|
| 基础数据操作 | SELECT, INSERT, UPDATE, DELETE | 增删改查表数据。 | 普通开发、后端应用账号。 |
| 库表结构管理 | CREATE, ALTER, DROP, INDEX | 建表、改结构、删表、建索引。 | DBA、高级开发(测试环境)。 |
| 高级系统管理 | PROCESS, RELOAD, SHUTDOWN | 查看进程、刷新配置、关库。 | 运维、DBA。 |
| 最高权限 | ALL PRIVILEGES | 拥有所有权限,还能给别人授权。 | 仅限 root,应用账号严禁给这个! |
核心操作:创建用户,赋予权限,回收权限,删除用户。
创建用户语法:create user 'username'@'localhost' identfied by 'password';
赋予权限语法#赋予全部权限grant all privileges on database_name.table_name to 'username'@'localhost'
#赋予指定库和权限grant select,insert on database_name.table_name to 'username'@'localhost'
回收权限#撤销全部权限revoke all privileges on database_name.* from 'username'@'localhost';#撤销部分权限revoke select,insert,update on database_name.* from 'username'@'localhost';
查看权限语法#查看权限show grants for 'username'@'localhost'#查看用户列表select user,host from mysql.user
删除用户语法drop user 用户名| 类别 | 关键字/名称 | 作用 | 举例 |
|---|---|---|---|
| DCL 关键字 | GRANT | 授权动作。用来给用户发权限。 | GRANT SELECT ON ... |
| DCL 关键字 | REVOKE | 回收动作。用来把发出去的权限收回来。 | REVOKE DROP ON ... |
| 具体权限名 | SELECT, INSERT, UPDATE… | 被授权的内容。写在 GRANT 后面,告诉数据库具体允许干啥。 | GRANT SELECT ON db.* TO user; |
3.数据备份与恢复
全量备份实现
#备份单表和单库mysqldump -u username -p database_name 表名> backup_sql
#备份单库和多表mysqldump -u username -p database_name 表1,表2···> backup_sql
#备份单库的所有表mysqldump -u username -p database_name > backup_sql
#例如mysqldump -uroot studb >d:/back.sql#-p如果写密码必须紧贴-p参数#以上命令必须在未连接mysql状态下执行全量恢复实现
mysql -u username -p databse_name <backup.sql#注意:需要提前准备数据库,导入已存在的库版本必须兼容binlog日志实现恢复
但记住:Binlog 不能单独使用,必须配合全量备份才能实现“时间点恢复”(PITR)。给你把这套“起死回生”的流程拆得明明白白,关键时刻能保命!
D:\MySQL\MySQL Server 8.0\my.ini上面的是mysql配置位置下面是设置存储位置# Path to the database rootdatadir=D:/MySQL/MySQL Server 8.0\Data
查看日志文件详细(cmd)mysqlbinlog -v binglog 日志文件在动手之前,先确认你有没有“后悔药”。登录 MySQL 执行:
SHOW VARIABLES LIKE 'log_bin';- 如果返回
ON:有救!继续往下看。 - 如果返回
OFF:没救。除非你有全量备份,否则删了就是真没了。现在立刻去修改my.cnf开启 binlog,别等下次出事再哭!
假设你的备份策略是每天凌晨 2:00 全量备份,结果下午 14:30 手滑删了表。恢复逻辑不是“撤销删除”,而是**“回到过去,重放历史”**:
- 回档:用凌晨 2:00 的全量备份,把数据库恢复到凌晨的状态。
- 重放:用 binlog 把凌晨 2:00 到下午 14:29:59(误操作前一刻)之间的所有操作重新执行一遍。
- 跳过:精准跳过那条误删的 SQL,数据就回来了。
实战操作步骤
第一步:恢复全量备份(建立基准)
千万别直接在原库上搞!找个临时实例或者新建一个库 temp_db,把最近一次的全量备份导进去。
# 假设你有凌晨2点的全量备份文件 full_backup.sqlmysql -u root -p temp_db < /backup/full_backup.sql此时,temp_db 里的数据停留在凌晨 2:00。
第二步:定位误操作位置(最关键!)
你需要找到那条“罪魁祸首”SQL 在 binlog 里的具体位置。
-
查看 binlog 文件列表:
SHOW BINARY LOGS;找到包含误操作时间段的那个文件(比如
mysql-bin.000023)。 -
解析 binlog 内容: 使用
mysqlbinlog工具把二进制日志转成可读的 SQL。推荐加上--base64-output=DECODE-ROWS -v,这样能看清具体的行变更。# 导出指定时间段的日志到文本文件分析mysqlbinlog --base64-output=DECODE-ROWS -v \--start-datetime="2026-05-22 02:00:00" \--stop-datetime="2026-05-22 15:00:00" \/var/lib/mysql/mysql-bin.000023 > /tmp/binlog_analysis.sql打开
/tmp/binlog_analysis.sql,搜索你的误操作(比如DROP TABLE或DELETE FROM orders)。你会看到类似这样的注释:sql
# at 123456#260522 14:30:00 server id 1 end_log_pos 123789 ...DELETE FROM orders WHERE ... <-- 找到这条!记下误操作之前的那个
end_log_pos(比如123456),这就是恢复的终点。
第三步:重放 binlog(精准回滚)
现在要把从全量备份时刻到误操作前一秒的所有变更,应用到 temp_db。
# 方案A:按时间点恢复(简单,但可能跨文件)mysqlbinlog --start-datetime="2026-05-22 02:00:00" \--stop-datetime="2026-05-22 14:29:59" \/var/lib/mysql/mysql-bin.000023 | mysql -u root -p temp_db
# 方案B:按位置恢复(更精确,推荐!)# 假设全量备份对应的 binlog position 是 107,误操作前的 position 是 123456mysqlbinlog --start-position=107 \--stop-position=123456 \/var/lib/mysql/mysql-bin.000023 | mysql -u root -p temp_db注意:如果 binlog 跨越了多个文件(比如从 000022 到 000023),必须按文件名顺序依次执行,不能乱序!
第四步:验证并导回生产库
在 temp_db 里查一下数据,确认误删的数据回来了,且后续的正常业务数据也没丢。确认无误后,把这部分差异数据导回原库,或者直接替换表。
“防翻车”终极警告
- 严禁在生产库直接重放 binlog!
这是大忌!直接在原库执行
mysqlbinlog | mysql,万一中间报错或者又执行了一遍误操作,数据彻底烂尾,神仙也救不回来。必须在临时实例或从库上恢复,验证好了再导回去。 - binlog 格式必须是 ROW 模式!
检查
my.cnf里的binlog_format。如果是STATEMENT模式,某些函数(如NOW()、UUID())在重放时会产生不一样的值,导致数据错乱。生产环境强制设为ROW,它记录的是每一行的具体变更,恢复最安全。 - 小心
expire_logs_days坑! MySQL 默认会清理过期的 binlog。如果你发现误删是一周前的事,但 binlog 只保留了 7 天,那很抱歉,日志可能已经被自动 purge 掉了。生产环境建议至少保留 7-14 天的 binlog,并且定期把旧日志备份到对象存储(如 OSS/S3)。 - GTID 环境的特殊处理
如果你的 MySQL 开启了 GTID(全局事务标识),直接用
mysqlbinlog回放可能会报GTID already exists错误。这时候必须加上--skip-gtids=true参数,告诉 MySQL 别管事务 ID,只管执行 SQL。
4.窗口函数
首先区分一下它和groupby
| 维度 | GROUP BY (聚合) | 窗口函数 (Window Function) |
|---|---|---|
| 结果行数 | 行数压缩。每组只返回一行汇总数据,明细直接丢失。 | 行数不变。原表有 100 行,结果还是 100 行,只是在每行旁边新增了一列计算结果。 |
| 核心逻辑 | “把数据压扁”。比如算部门平均工资,你只能看到每个部门的平均数,看不到具体是谁。 | “保留明细的同时做聚合”。比如算部门平均工资,你能看到每个员工的工资,同时旁边多了一列显示他所在部门的平均工资,方便对比。 |
| 适用场景 | 简单的统计报表,如“统计每个部门的总人数”。 | 复杂分析,如“查询每个部门薪资排名前 3 的员工”、“计算每日销售额的累计值”、“计算环比增长率”。 |
| 执行顺序 | 在 WHERE 之后执行。 | 在 WHERE、GROUP BY、HAVING 之后执行,在 ORDER BY 之前执行。维度 |
窗口函数速查
| 门派分类 | 核心函数 | 通俗解释 | 典型应用场景 |
|---|---|---|---|
| 🔢 排名函数 | ROW_NUMBER() RANK() DENSE_RANK() NTILE(n) | 发号牌。给窗口内的每一行数据发一个序号或等级。 | 分组取 Top N、去重、成绩排名、薪资分档。 |
| ➕ 聚合窗口函数 | SUM() AVG() COUNT() MAX() / MIN() | 不压行的聚合。在保留明细的同时,算出累计值、移动平均值。 | 累计销售额、移动平均线、当前行占总额比例。 |
| ↔️ 偏移函数 | LAG(col, n) LEAD(col, n) FIRST_VALUE() LAST_VALUE() | 穿越时空。能拿到当前行“前一行”或“后一行”的值,或者窗口的首尾值。 | 计算环比/同比增长率、相邻订单时间间隔、标记首单/末单。 |
| 📈 分布函数 | PERCENT_RANK() CUME_DIST() | 算百分比。看当前行在整个窗口里处于什么相对位置。 | 统计学分析、判断某员工薪资是否超过全公司 80% 的人。 |
1. 排名函数:解决“并列”与“跳号”的纠葛
这是面试和实战中最高频的考点!假设一个部门有 4 个人,薪资分别是 20000, 20000, 15000, 10000,这三个函数的表现截然不同:
ROW_NUMBER():铁面无私。不管薪资是否相同,强制给出唯一连续序号1, 2, 3, 4。适合分页、去重(比如每个用户只保留最新一条订单)。RANK():比赛逻辑。两人并列第 1,下一个直接跳到第 3,结果1, 1, 3, 4。中间会跳号。DENSE_RANK():阶梯逻辑。两人并列第 1,下一个紧挨着是第 2,结果1, 1, 2, 3。不跳号,适合做等级评定。NTILE(n):均匀分桶。把窗口内的数据强行切成 n 份。比如NTILE(4)就是把薪资从高到低切成 4 个四分位,适合做薪资分层分析。
2. 聚合窗口函数:明细与汇总同屏
普通 SUM() 会把多行压成一行,但加上 OVER() 后,它能在每一行旁边附加一个聚合结果,行数绝不减少。
- 累计求和:
SUM(amount) OVER (ORDER BY order_date)。默认范围是从分区第一行累加到当前行,轻松算出每日累计营收。 - 移动平均:
AVG(amount) OVER (ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)。明确指定物理行范围(前2行+当前行),算出近3天的移动平均值,平滑波动。 - 占比计算:
amount / SUM(amount) OVER ()。分母不带PARTITION BY就是全表总和,直接算出每笔订单占全局总金额的比例。
3. 偏移函数:告别自连接
以前算“本月比上月增长多少”,得把表自连接一次,还要处理月初月末的边界。现在用偏移函数直接跨行取值:
LAG(col, 1):取前 1 行的值。第一行没有前值,默认返回NULL。LEAD(col, 1):取后 1 行的值。最后一行没有后值,默认返回NULL。- 实战公式:
(本月销售额 - LAG(本月销售额, 1) OVER (ORDER BY 月份)) / LAG(本月销售额, 1) OVER (ORDER BY 月份)。一行语句直接算出环比增长率,无需任何应用层代码。
4. 首尾值函数的巨坑预警
FIRST_VALUE() 和 LAST_VALUE() 看起来简单,但 LAST_VALUE() 有个致命陷阱!
它的默认帧范围是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(从起点到当前行)。这意味着,如果你不手动指定范围,当前行的 LAST_VALUE 往往就是它自己!
正确写法:想取整个分区的最后一个值,必须显式指定范围到无穷远:
部分信息可能已经过时









