SQL 学习笔记
阅读方式
- 第 0 章是速查表,随时回来翻;
- 第 1~3 章是基础(概念 / 数据类型 / 建表改表);
- 第 4~7 章是查询与函数(日常写得最多);
- 第 8~10 章是高级(窗口函数 / 索引 / 事务锁);
- 第 11~15 章是设计、避坑、方言、面试、附录。
- 难度用 ⭐ 标注,⭐ 越多越难。难点一律用表格呈现。
全文示例表统一(先看一眼,后面所有 SQL 都基于它们):
employees(员工表) departments(部门表)
┌────────┬────────┬─────────┬────────┬────────────┬────────────┐ ┌─────────┬───────────┬──────────┐
│ emp_id │ name │ dept_id │ salary │ hire_date │ manager_id │ │ dept_id │ dept_name │ location │
├────────┼────────┼─────────┼────────┼────────────┼────────────┤ ├─────────┼───────────┼──────────┤
│ 1 │ 张三 │ 10 │ 8000 │ 2021-03-01 │ 3 │ │ 10 │ 技术部 │ 北京 │
│ 2 │ 李四 │ 10 │ 12000 │ 2020-07-15 │ 3 │ │ 20 │ 市场部 │ 上海 │
│ 3 │ 王五 │ 20 │ 15000 │ 2019-01-20 │ NULL │ │ 30 │ 财务部 │ 北京 │
│ 4 │ 赵六 │ 30 │ 9000 │ 2022-11-05 │ 3 │ └─────────┴───────────┴──────────┘
│ 5 │ 孙七 │ NULL │ 7000 │ 2023-02-10 │ NULL │
└────────┴────────┴─────────┴────────┴────────────┴────────────┘
orders(订单表,用于分组/窗口函数示例)
┌──────────┬─────────┬────────┬────────────┬────────┐
│ order_id │ user_id │ amount │ order_date │ status │
├──────────┼─────────┼────────┼────────────┼────────┤
│ 1001 │ 1 │ 120.50 │ 2024-01-05 │ 已付款 │
│ 1002 │ 1 │ 300.00 │ 2024-02-11 │ 已付款 │
│ 1003 │ 2 │ 80.00 │ 2024-01-20 │ 已退款 │
└──────────┴─────────┴────────┴────────────┴────────┘
方言说明 :正文默认 MySQL 8.0。关键差异在第 13 章统一对比。
〇、常用命令速查表(先看这个)
0.1 六大基本动作
| 动作 |
语法模板 |
说明 |
| 查 DQL |
SELECT 列 FROM 表 WHERE 条件; |
最高频,占日常 80% |
| 增 DML |
INSERT INTO 表(列1,列2) VALUES(值1,值2); |
批量:VALUES(...),(...); |
| 改 DML |
UPDATE 表 SET 列=新值 WHERE 条件; |
忘写 WHERE = 全表更新 |
| 删 DML |
DELETE FROM 表 WHERE 条件; |
忘写 WHERE = 全表清空 |
| 建 DDL |
CREATE TABLE 表(列 类型 约束, ...); |
见 §3 |
| 改结构 DDL |
ALTER TABLE 表 ADD/MODIFY/CHANGE/DROP 列; |
加字段、改类型 |
0.2 查询语句标准骨架(背下来)
SELECT [DISTINCT] 列, 聚合函数(列)
FROM 表 [别名]
[JOIN 另一张表 [别名] ON 连接条件]
WHERE 行级过滤条件
GROUP BY 分组列
HAVING 分组后过滤条件
ORDER BY 排序列 [ASC|DESC]
LIMIT 条数 [OFFSET 偏移];
0.3 一句话速查(按使用频率排序)
| 我想做... |
写法 |
难度 |
| 查所有列 |
SELECT * FROM t; |
⭐ |
| 去重 |
SELECT DISTINCT col FROM t; |
⭐ |
| 模糊匹配 |
WHERE name LIKE '张%'; |
⭐ |
| 多值匹配 |
WHERE dept_id IN (10,20); |
⭐ |
| 区间 |
WHERE salary BETWEEN 8000 AND 15000; |
⭐ |
| 空值判断 |
WHERE x IS NULL / IS NOT NULL |
⭐ |
| 排序 |
ORDER BY salary DESC; |
⭐ |
| 取前 N 行 |
LIMIT 10; |
⭐ |
| 分页 |
LIMIT 10 OFFSET 20; |
⭐⭐ |
| 建表 |
CREATE TABLE t(id INT PRIMARY KEY AUTO_INCREMENT, ...); |
⭐⭐ |
| 加字段 |
ALTER TABLE t ADD COLUMN age INT DEFAULT 0; |
⭐⭐ |
| 建索引 |
CREATE INDEX idx_name ON t(col); |
⭐⭐ |
| 计数 |
COUNT(*)、COUNT(DISTINCT col) |
⭐⭐ |
| 分组统计 |
GROUP BY dept_id |
⭐⭐ |
| 分组后过滤 |
HAVING COUNT(*) > 2 |
⭐⭐ |
| 两表关联 |
JOIN d ON e.dept_id = d.dept_id |
⭐⭐ |
| 插入更新二选一 |
INSERT ... ON DUPLICATE KEY UPDATE ... |
⭐⭐⭐ |
| 拼接字符串 |
CONCAT(a,'-',b) |
⭐⭐ |
| 分组内合并成串 |
GROUP_CONCAT(name) |
⭐⭐⭐ |
| 条件分支 |
CASE WHEN ... THEN ... ELSE ... END |
⭐⭐⭐ |
| 组内排名 |
ROW_NUMBER() OVER(PARTITION BY x ORDER BY y) |
⭐⭐⭐⭐ |
| 同比环比 |
LAG(x,1) OVER(ORDER BY y) |
⭐⭐⭐⭐ |
| 复杂中间结果 |
WITH t AS (SELECT ...) SELECT ... FROM t |
⭐⭐⭐⭐ |
| 看执行计划 |
EXPLAIN SELECT ...; |
⭐⭐⭐⭐ |
0.4 常用函数速查
| 类别 |
函数 |
作用 |
| 聚合 |
COUNT SUM AVG MAX MIN |
计数/求和/均值/最大/最小 |
| 字符串 |
CONCAT SUBSTRING TRIM UPPER LOWER REPLACE LENGTH LOCATE |
拼接/截取/去空格/大小写/替换/长度/定位 |
| 数值 |
ROUND CEIL FLOOR ABS MOD TRUNCATE |
四舍五入/上取整/下取整/绝对值/取余/截断 |
| 日期 |
NOW() CURDATE() DATE_ADD DATEDIFF DATE_FORMAT TIMESTAMPDIFF |
当前时间/今天/加减/差值/格式化 |
| 条件 |
IFNULL(x,0) COALESCE(x,...) NULLIF(a,b) IF(c,a,b) CASE WHEN |
空值兜底/分支 |
| 转换 |
CAST(x AS 类型) CONVERT(x, 类型) |
显式类型转换 |
| 分组 |
GROUP_CONCAT |
组内值合并成字符串 |
| 窗口 |
ROW_NUMBER RANK DENSE_RANK LAG LEAD SUM() OVER |
见 §8.2 |
| JSON |
JSON_EXTRACT JSON_OBJECT -> ->> |
见 §7.6 |
0.5 数据类型速查表(建表时对照)⭐⭐
| 想存什么 |
推荐类型 |
理由 |
| 主键 ID |
BIGINT UNSIGNED AUTO_INCREMENT 或 INT |
自增整数,索引效率高 |
| 姓名 / 标题 / 邮箱 |
VARCHAR(50~255) |
长度可变且不长 |
| 手机号 / 身份证 / MD5 / UUID |
CHAR(11) / CHAR(18) / CHAR(32) |
定长用 CHAR,省长度字节 |
| 状态 / 类型码(少量枚举) |
TINYINT 或 ENUM |
占 1 字节 |
| 布尔 |
TINYINT(1) |
MySQL 无原生 BOOLEAN |
| 金额 |
DECIMAL(10,2) |
绝不能用 FLOAT/DOUBLE |
| 百分比 / 比率 |
DECIMAL(5,4) |
精度可控 |
| 计数 / 年龄 |
INT |
范围足够 |
| 创建时间 |
DATETIME 或 TIMESTAMP |
见 §2.4 |
| 纯日期(生日) |
DATE |
3 字节 |
| 文章内容 / 长描述 |
TEXT |
超过 VARCHAR 就上 TEXT |
| 图片 / 文件 |
不建议存库 ,只存路径 VARCHAR |
数据库只存 URL/路径 |
| 灵活结构 |
JSON |
MySQL 5.7+ 原生支持 |
0.6 DDL 一句话速查 ⭐⭐
| 操作 |
语句 |
| 建库 |
CREATE DATABASE db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; |
| 删库 |
DROP DATABASE db; |
| 建表 |
CREATE TABLE t (...); |
| 复制表结构 |
CREATE TABLE t2 LIKE t1; |
| 复制表结构+数据 |
CREATE TABLE t2 AS SELECT * FROM t1; |
| 加列 |
ALTER TABLE t ADD COLUMN c INT AFTER b; |
| 改列类型 |
ALTER TABLE t MODIFY COLUMN c VARCHAR(100); |
| 改列名+类型 |
ALTER TABLE t CHANGE COLUMN old new INT; |
| 删列 |
ALTER TABLE t DROP COLUMN c; |
| 改表名 |
RENAME TABLE old TO new; |
| 清空表 |
TRUNCATE TABLE t; |
| 删表 |
DROP TABLE IF EXISTS t; |
| 建索引 |
CREATE INDEX idx ON t(c1,c2); |
| 删索引 |
DROP INDEX idx ON t; |
| 看建表语句 |
SHOW CREATE TABLE t; |
| 看表结构 |
DESC t; / SHOW COLUMNS FROM t; |
一、基础概念 ⭐
1.1 SQL 五类语句
| 分类 |
全称 |
作用 |
代表语句 |
| DQL |
Data Query Language |
查询 |
SELECT |
| DML |
Data Manipulation Language |
增删改数据 |
INSERT UPDATE DELETE |
| DDL |
Data Definition Language |
定义/修改表结构 |
CREATE ALTER DROP TRUNCATE RENAME |
| DCL |
Data Control Language |
权限 |
GRANT REVOKE |
| TCL |
Transaction Control Language |
事务 |
BEGIN COMMIT ROLLBACK SAVEPOINT |
💡 面试常问:DDL 与 DML 最大区别是 DDL 会隐式提交事务,无法回滚。
1.2 DELETE / TRUNCATE / DROP 对比(高频考点)
| 对比项 |
DELETE |
TRUNCATE |
DROP |
| 分类 |
DML |
DDL |
DDL |
| 删除什么 |
按条件删行 |
清空所有行,保留表结构 |
删除整张表(结构+数据+索引) |
| 能否加 WHERE |
✅ |
❌ |
❌ |
| 能否回滚 |
✅ 可回滚 |
❌ 隐式提交,不可回滚 |
❌ 不可回滚 |
| 是否记日志 |
✅ 逐行记 undo/binlog(慢) |
❌ 只记少量(快) |
--- |
| 自增 ID |
不重置 |
重置为 1 |
--- |
| 触发器 |
✅ 触发 |
❌ 不触发 |
❌ |
| 释放表空间 |
❌ 需 OPTIMIZE TABLE |
✅ 立即释放 |
✅ |
| 速度 |
慢 |
快 |
快 |
1.3 术语对照表
| 术语 |
含义 |
类比 |
| Database / Schema |
数据库(MySQL 中二者基本等价) |
一个 Excel 文件 |
| Table |
表 |
Excel 中的一个 sheet |
| Row / Record |
行 / 记录 |
sheet 中的一行 |
| Column / Field |
列 / 字段 |
sheet 中的一列 |
| Primary Key |
主键,唯一标识一行 |
身份证号 |
| Foreign Key |
外键,指向另一表主键 |
引用别的 sheet 的 ID |
| Index |
索引,加速查询 |
书的目录 |
| View |
视图,虚拟表 |
保存好的查询结果 |
| Transaction |
事务,一组原子操作 |
转账的"扣款+入账" |
1.4 命名规范与注释
-- 单行注释(-- 后必须有空格)
# MySQL 特有的单行注释
/* 多行
注释 */
-- 推荐命名:小写 + 下划线,见名知意
CREATE TABLE user_order (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
created_at DATETIME NOT NULL
);
| 规范项 |
建议 |
| 大小写 |
关键字大写、表名列名小写(可读性) |
| 分隔符 |
下划线 user_id,别用驼峰 |
| 布尔字段 |
用 is_ 前缀:is_deleted |
| 时间字段 |
用 _at 后缀:created_at、updated_at |
| 禁止 |
用保留字做列名(order、desc、status 需加反引号或改名) |
| 长度 |
表名 ≤ 32 字符 |
二、数据类型(重点补充)⭐⭐
2.1 三大类概览
| 大类 |
具体类型 |
常见用途 |
| 数值 |
TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT、DECIMAL、FLOAT/DOUBLE、BIT |
ID、数量、金额、比率 |
| 字符串 |
CHAR、VARCHAR、TEXT 家族、ENUM、SET |
姓名、地址、正文 |
| 日期时间 |
DATE、TIME、DATETIME、TIMESTAMP、YEAR |
生日、下单时间 |
| 二进制 |
BINARY、VARBINARY、BLOB 家族 |
图片、文件、加密数据 |
| 特殊 |
JSON、GEOMETRY(空间) |
灵活结构、地理位置 |
2.2 CHAR vs VARCHAR vs TEXT(★重点,最容易搞混)
CREATE TABLE demo (
code CHAR(6), -- 定长 6 字符
name VARCHAR(50), -- 变长,最多 50 字符
content TEXT -- 长文本
);
核心对比表
| 对比维度 |
CHAR(n) |
VARCHAR(n) |
TEXT |
| 存储方式 |
定长,不足补空格到 n |
变长,实际长度 + 1~2 字节长度前缀 |
变长,过长时存行外(溢出页),行内只留指针 |
| 最大长度 |
255 字符 |
受行总 65535 字节限制(utf8mb4 下约 16383 字符) |
TEXT 64KB / MEDIUMTEXT 16MB / LONGTEXT 4GB |
| 空间占用 |
声明多少占多少(可能浪费) |
按实际内容(省空间) |
按实际内容 |
| 尾随空格 |
存入时补空格,读取时自动去掉 |
原样保留 |
原样保留 |
| 默认值 DEFAULT |
✅ 支持 |
✅ 支持 |
❌ MySQL 中不支持 |
| 索引 |
整列可建索引 |
整列可建(过长需前缀索引) |
⚠️ 必须指定前缀长度 INDEX(content(20)) |
| 排序比较 |
全列参与 |
全列参与 |
只比较前 max_sort_length(默认 1024)字节 |
| 内存临时表 |
定长,引擎友好 |
变长 |
大字段易导致内存临时表转磁盘临时表(慢) |
| WHERE / ORDER 性能 |
最快(定长、无碎片) |
快 |
慢(尤其排序、去重) |
| UPDATE 碎片 |
无 |
变长更新可能产生行碎片 |
可能产生 |
| 能否整行存内存 |
✅ |
✅ |
超阈值时只存指针,需额外 IO |
选择建议表
| 场景 |
选什么 |
为什么 |
| 手机号(11 位)、身份证(18 位) |
CHAR(11) / CHAR(18) |
长度固定,CHAR 省掉长度前缀字节,定长查找更快 |
| 国家代码、状态码、性别 |
CHAR(2) / CHAR(1) |
定长且极短 |
| MD5(32)、UUID(36) |
CHAR(32) / CHAR(36) |
定长 |
| 姓名、邮箱、标题、地址 |
VARCHAR(50~255) |
长度变化大 |
| 商品描述、短备注 |
VARCHAR(500~2000) |
仍在单行舒适区内 |
| 文章内容、评论正文、日志、大 JSON |
TEXT / MEDIUMTEXT |
超过 VARCHAR 舒适区 |
频繁被 WHERE/ORDER BY/GROUP BY 的字段 |
绝不用 TEXT |
会拖垮性能 |
必须知道的 6 个细节
① VARCHAR(n) 里的 n 是「字符数」不是「字节数」(MySQL 5.0.3 之后)
-- utf8mb4 下一个汉字占 4 字节
VARCHAR(10) -- 最多存 10 个汉字 或 10 个英文字母,但最多占 40 字节
② 长度前缀是 1 字节还是 2 字节?
| 列定义的最大字节数 |
长度前缀 |
≤ 255 字节(如 utf8mb4 的 VARCHAR(63)) |
1 字节 |
| > 255 字节 |
2 字节 |
所以能省则省:utf8mb4 下 VARCHAR(255) 比 VARCHAR(256) 每个值省 1 字节。
③ 行总长度限制 65535 字节
-- ❌ 报错:Row size too large
CREATE TABLE bad (a VARCHAR(20000), b VARCHAR(20000)) CHARSET utf8mb4;
-- utf8mb4: 20000*4 + 20000*4 = 160000 字节 > 65535
TEXT / BLOB 在行内只占 9~12 字节指针,不计入这个限制 ------ 这也是长文本必须用 TEXT 的原因之一。
④ CHAR 的尾随空格"吃掉"现象
CREATE TABLE t (c CHAR(5), v VARCHAR(5));
INSERT INTO t VALUES ('ab ', 'ab ');
SELECT CONCAT('[', c, ']'), CONCAT('[', v, ']') FROM t;
-- 结果:[ab] [ab ] ← CHAR 的尾随空格被去掉了!
如果尾随空格有业务含义(如定长文件导入),改用 VARCHAR。
⑤ TEXT 家族容量表
| 类型 |
最大长度 |
约等于 |
TINYTEXT |
255 字节 |
短句 |
TEXT |
64 KB |
一篇文章 |
MEDIUMTEXT |
16 MB |
一本书 |
LONGTEXT |
4 GB |
极大文本 |
⑥ TEXT vs BLOB
| 对比 |
TEXT |
BLOB |
| 内容 |
文本,有字符集和排序规则 |
二进制字节流,无字符集 |
| 比较排序 |
按字符序 |
按字节 |
| 用途 |
文章、描述 |
图片、文件、加密数据 |
⑦ 性能最佳实践:TEXT 垂直拆分
-- ✅ 推荐:主表只放常用小字段,正文单独一张表
CREATE TABLE article (id BIGINT PRIMARY KEY, title VARCHAR(200), author_id BIGINT, created_at DATETIME);
CREATE TABLE article_content (article_id BIGINT PRIMARY KEY, content MEDIUMTEXT);
列表页只查 article 很快,点进详情才查 article_content。
2.3 数值类型
整数类型对照表
| 类型 |
字节 |
有符号范围 |
无符号范围 |
用途 |
TINYINT |
1 |
-128 ~ 127 |
0 ~ 255 |
状态码、年龄、布尔 |
SMALLINT |
2 |
±32767 |
0 ~ 65535 |
小计数 |
MEDIUMINT |
3 |
±838 万 |
0 ~ 1677 万 |
中等计数 |
INT / INTEGER |
4 |
±21 亿 |
0 ~ 42 亿 |
最常用 |
BIGINT |
8 |
±922 亿亿 |
0 ~ 1844 亿亿 |
主键、大额、雪花 ID |
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT -- 无符号,范围翻倍
age TINYINT UNSIGNED -- 0~255,够用且省空间
is_deleted TINYINT(1) DEFAULT 0 -- 当作布尔用
⚠️ INT(11) 里的 11 不是长度! 那是"显示宽度",配合 ZEROFILL 才有用,MySQL 8.0.17 起已废弃。存储范围只由类型本身(INT)决定。
DECIMAL vs FLOAT vs DOUBLE(★金额必考)
| 对比 |
DECIMAL(M,D) |
FLOAT |
DOUBLE |
| 精度 |
精确定点数 |
单精度近似(约 7 位有效数字) |
双精度近似(约 15 位) |
| 是否有误差 |
❌ 无 |
✅ 有 |
✅ 有 |
| 存储 |
变长(每 4 字节存 9 位) |
4 字节 |
8 字节 |
| 计算速度 |
慢(十进制运算) |
快 |
快 |
| 适用场景 |
金额、汇率、税率 |
科学计算 |
科学计算 |
-- 灾难演示
SELECT 0.1 + 0.2 = 0.3; -- 结果 0(FALSE!浮点误差)
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2))
= CAST(0.3 AS DECIMAL(10,2)); -- 结果 1(TRUE)
| 参数 |
含义 |
示例 |
M |
总位数(精度),最大 65 |
DECIMAL(10,2) → 共 10 位 |
D |
小数位数(标度),最大 30 |
→ 小数 2 位,整数 8 位 |
铁律:凡是涉及钱、必须精确对账的,一律 DECIMAL。
2.4 日期时间类型(★重点)
| 类型 |
字节 |
格式 |
范围 |
时区敏感 |
用途 |
DATE |
3 |
YYYY-MM-DD |
1000-01-01 ~ 9999-12-31 |
❌ |
生日、纯日期 |
TIME |
3 |
HH:MM:SS |
-838:59:59 ~ 838:59:59 |
❌ |
时长、营业时间 |
YEAR |
1 |
YYYY |
1901 ~ 2155 |
❌ |
年份 |
DATETIME |
8 |
YYYY-MM-DD HH:MM:SS |
1000 ~ 9999 年 |
❌ 不随时区变 |
推荐:下单时间、创建时间 |
TIMESTAMP |
4 |
YYYY-MM-DD HH:MM:SS |
1970 ~ 2038-01-19 |
✅ 随时区转换 |
需要自动更新 / 跨时区 |
DATETIME vs TIMESTAMP 深度对比
| 对比项 |
DATETIME |
TIMESTAMP |
| 存储空间 |
8 字节 |
4 字节(省一半) |
| 时间范围 |
1000~9999 年 |
1970~2038(2038 问题) |
| 时区 |
存什么读什么,不转换 |
存时转 UTC,读时转回当前时区 |
| NULL |
可为 NULL |
默认 NOT NULL,且自动填充当前时间 |
| 自动更新 |
需显式写 ON UPDATE CURRENT_TIMESTAMP |
默认第一个 TIMESTAMP 列会自动设置 |
| 索引效率 |
略低 |
略高 |
CREATE TABLE t (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
birthday DATE,
duration TIME
);
2038 问题 :TIMESTAMP 用 32 位存秒,2038-01-19 03:14:07 后溢出。长期系统建议 DATETIME。
2.5 ENUM / SET
| 类型 |
含义 |
存储 |
示例 |
ENUM |
单选,从预设值里选一个 |
1~2 字节(存下标) |
ENUM('男','女','未知') |
SET |
多选,可选多个 |
1~8 字节(位图) |
SET('read','write','delete') |
CREATE TABLE user (
gender ENUM('男','女','未知') DEFAULT '未知',
perms SET('read','write','delete')
);
INSERT INTO user VALUES ('男', 'read,write');
| 优点 |
缺点 |
| 省空间、自带约束(非法值插不进) |
增删枚举值要 ALTER TABLE,线上有风险 |
| 可读性好 |
排序按下标而非字母序,反直觉 |
|
跨数据库移植差 |
实践建议 :变动频繁的状态码用 TINYINT + 字典表,而不是 ENUM。
2.6 JSON 类型(MySQL 5.7+)
CREATE TABLE config (id BIGINT PRIMARY KEY, data JSON);
INSERT INTO config VALUES (1, '{"theme":"dark","lang":"zh"}');
SELECT data->>'$.theme' FROM config; -- 'dark'(去引号)
SELECT JSON_EXTRACT(data, '$.lang') FROM config; -- "zh"(带引号)
SELECT * FROM config WHERE data->>'$.theme' = 'dark';
| 函数 |
作用 |
JSON_EXTRACT(j,path) / j->path |
取值(带引号) |
j->>path |
取值(自动去引号) |
JSON_OBJECT(k,v,...) |
构造 JSON 对象 |
JSON_ARRAY(v,...) |
构造 JSON 数组 |
JSON_SET / JSON_REPLACE / JSON_REMOVE |
修改 |
JSON_CONTAINS(j,val,path) |
是否包含 |
JSON_LENGTH(j) |
元素个数 |
⚠️ JSON 列不能直接建索引,需借助生成列间接索引:
ALTER TABLE config ADD COLUMN theme VARCHAR(20)
GENERATED ALWAYS AS (data->>'$.theme') STORED;
CREATE INDEX idx_theme ON config(theme);
2.7 隐式类型转换陷阱表 ⭐⭐⭐
| 场景 |
后果 |
例子 |
| 字符串列 vs 数字比较 |
索引失效,全表扫描 |
WHERE phone = 13800138000(phone 是 VARCHAR) |
| VARCHAR 列存数字后排序 |
按字典序:'10' < '9' |
ORDER BY varchar_col |
| 日期列 vs 字符串 |
一般可转换,格式错则变 NULL |
'2024/01/01' 不被识别 |
+ 运算 |
字符串被强转数字,非数字变 0 |
'1abc' + 1 = 2(有 warning) |
口诀:比较时两侧类型必须一致,否则索引白建。
2.8 类型选择总原则
| 原则 |
说明 |
| 越小越好 |
能用 TINYINT 就别用 INT,省磁盘、省内存、省索引空间 |
| 简单越好 |
整数 > 字符串;IP 用 INT UNSIGNED 而非 VARCHAR(15) |
| 避免 NULL |
尽量 NOT NULL DEFAULT '',NULL 让索引、统计、比较都变复杂 |
| 金额用 DECIMAL |
绝不用浮点 |
| 定长用 CHAR |
手机号、编码、MD5 |
| 长文本拆表 |
TEXT 独立出去 |
三、DDL 数据定义语言 ⭐⭐
3.1 CREATE TABLE 完整语法
CREATE TABLE IF NOT EXISTS employee (
emp_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '员工ID',
name VARCHAR(50) NOT NULL COMMENT '姓名',
dept_id INT COMMENT '部门ID',
salary DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '月薪',
hire_date DATE NOT NULL COMMENT '入职日期',
manager_id BIGINT UNSIGNED COMMENT '上级ID',
PRIMARY KEY (emp_id),
UNIQUE KEY uk_name_dept (name, dept_id),
KEY idx_dept (dept_id),
CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='员工表';
| 组成部分 |
说明 |
IF NOT EXISTS |
表已存在时不报错 |
| 列定义 |
列名 类型 [约束] [DEFAULT 默认值] [COMMENT 注释] |
PRIMARY KEY |
主键 |
UNIQUE KEY |
唯一索引 |
KEY / INDEX |
普通索引 |
FOREIGN KEY |
外键 |
ENGINE=InnoDB |
存储引擎(默认,支持事务 + 行锁) |
CHARSET=utf8mb4 |
必须 utf8mb4 (utf8 是残缺的 3 字节,存不了 emoji) |
COLLATE |
排序规则,utf8mb4_unicode_ci 不区分大小写 |
3.2 约束详解表(★重点)
| 约束 |
关键字 |
作用 |
说明 |
| 主键 |
PRIMARY KEY |
唯一标识一行 |
隐含 NOT NULL + UNIQUE;一表仅一个;InnoDB 中是聚簇索引 |
| 外键 |
FOREIGN KEY |
引用另一表主键 |
保证引用完整性;大厂常禁用(性能与分库限制) |
| 唯一 |
UNIQUE |
值不可重复 |
允许多个 NULL;一表可多个 |
| 非空 |
NOT NULL |
不能为空 |
强烈建议都加 |
| 默认 |
DEFAULT |
没给值时的兜底 |
DEFAULT 0、DEFAULT CURRENT_TIMESTAMP |
| 自增 |
AUTO_INCREMENT |
自动递增 |
必须作用在索引列(通常主键) |
| 检查 |
CHECK |
自定义条件 |
CHECK (salary > 0);MySQL 8.0.16 才真正生效 |
-- 组合主键
PRIMARY KEY (order_id, product_id)
-- 建表后加/删约束
ALTER TABLE employee ADD CONSTRAINT uk_email UNIQUE (email);
ALTER TABLE employee ADD CONSTRAINT ck_salary CHECK (salary >= 0);
ALTER TABLE employee DROP CHECK ck_salary;
PRIMARY KEY vs UNIQUE
| 对比 |
PRIMARY KEY |
UNIQUE |
| 数量 |
每表 1 个 |
可多个 |
| NULL |
❌ 不允许 |
✅ 允许多个 NULL |
| 索引类型 |
聚簇索引(InnoDB) |
二级索引 |
| 语义 |
行的身份 |
业务唯一性 |
3.3 ALTER TABLE 常见操作
ALTER TABLE employee ADD COLUMN email VARCHAR(100) AFTER name; -- 加列并指定位置
ALTER TABLE employee ADD COLUMN age TINYINT FIRST; -- 加到最前
ALTER TABLE employee MODIFY COLUMN name VARCHAR(100) NOT NULL; -- 改类型/约束
ALTER TABLE employee CHANGE COLUMN name full_name VARCHAR(100); -- 改列名+类型
ALTER TABLE employee ALTER COLUMN salary SET DEFAULT 5000; -- 设默认值
ALTER TABLE employee DROP COLUMN age; -- 删列
ALTER TABLE employee RENAME TO staff; -- 改表名
ALTER TABLE employee ADD INDEX idx_salary (salary); -- 加索引
ALTER TABLE employee CHARACTER SET = utf8mb4; -- 改字符集
⚠️ 线上变更风险 :大表 ALTER 会锁表(MySQL 8.0 支持 ALGORITHM=INSTANT 秒级加列,但改类型仍需重建表)。生产建议用 pt-online-schema-change 或 gh-ost。
3.4 索引的 DDL
CREATE INDEX idx_dept_salary ON employee(dept_id, salary); -- 联合索引
CREATE UNIQUE INDEX uk_code ON product(code); -- 唯一索引
CREATE FULLTEXT INDEX ft_content ON article(content); -- 全文索引
ALTER TABLE employee ADD INDEX idx_name(name(10)); -- 前缀索引
DROP INDEX idx_dept_salary ON employee;
SHOW INDEX FROM employee;
四、DML 数据操作 ⭐⭐
4.1 INSERT 插入
-- 单行全列
INSERT INTO employees VALUES (1,'张三',10,8000.00,'2021-03-01',3);
-- 指定列(推荐)
INSERT INTO employees (name, dept_id, salary, hire_date)
VALUES ('李四', 10, 12000.00, '2020-07-15');
-- 批量(效率高很多)
INSERT INTO employees (name, dept_id, salary) VALUES
('王五', 20, 15000),
('赵六', 30, 9000),
('孙七', NULL, 7000);
-- 从查询结果插入
INSERT INTO employee_bak (name, salary)
SELECT name, salary FROM employees WHERE dept_id = 10;
四种"冲突处理"写法对比(★重点)
| 写法 |
遇到唯一键冲突时 |
说明 |
INSERT INTO |
直接报错,整条失败 |
默认行为 |
INSERT IGNORE INTO |
静默跳过 |
有 warning,其他错误也会被忽略(慎用) |
INSERT ... ON DUPLICATE KEY UPDATE |
执行 UPDATE |
最常用(upsert) |
REPLACE INTO |
先 DELETE 再 INSERT |
⚠️ 会删旧行,自增 ID 变化、触发删除触发器 |
-- upsert:存在则累加,不存在则插入
INSERT INTO daily_stat (stat_date, pv)
VALUES ('2024-01-01', 1)
ON DUPLICATE KEY UPDATE pv = pv + 1;
4.2 UPDATE 更新
-- 单表
UPDATE employees SET salary = salary * 1.1 WHERE dept_id = 10;
-- 关联更新(JOIN)
UPDATE employees e
JOIN departments d ON e.dept_id = d.dept_id
SET e.salary = e.salary * 1.2
WHERE d.location = '北京';
-- 带 LIMIT,防止误伤全表
UPDATE employees SET salary = 8000 WHERE dept_id IS NULL LIMIT 10;
-- 按排序更新
UPDATE employees SET salary = 8000 ORDER BY hire_date LIMIT 5;
⚠️ 安全习惯 :先写 SELECT 验证 WHERE 条件命中行数,确认无误再改成 UPDATE。MySQL 开启 sql_safe_updates 后,不带主键条件的 UPDATE/DELETE 会被拒绝。
4.3 DELETE 删除
DELETE FROM employees WHERE emp_id = 5;
DELETE FROM employees WHERE dept_id IS NULL LIMIT 100; -- 分批删,避免长事务
-- 多表删除
DELETE e
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE d.location = '上海';
4.4 外键级联行为表
| 动作 |
含义 |
CASCADE |
主表删/改,从表跟着删/改 |
SET NULL |
主表删/改,从表外键置 NULL(要求该列可为 NULL) |
RESTRICT / NO ACTION |
默认,从表还有引用则禁止主表删/改 |
SET DEFAULT |
置为默认值(InnoDB 不支持) |
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE SET NULL
ON UPDATE CASCADE
4.5 软删除 vs 硬删除
| 方式 |
做法 |
优点 |
缺点 |
| 硬删除 |
DELETE |
干净、省空间 |
不可恢复,破坏外键 |
| 软删除(推荐) |
UPDATE SET is_deleted=1 |
可恢复、保留历史 |
表变大,查询都要带 is_deleted=0 |
五、DQL 基础查询 ⭐
5.1 SELECT / FROM
SELECT * FROM employees; -- 查所有列
SELECT name, salary FROM employees; -- 指定列
SELECT name AS 姓名, salary * 12 AS 年薪 FROM employees; -- 别名 + 运算
SELECT DISTINCT dept_id FROM employees; -- 去重
SELECT * 的四宗罪 :① 多传无用列,浪费网络/IO;② 无法走覆盖索引;③ 表结构变更时程序易崩;④ 无法看清依赖。
5.2 WHERE 运算符表
| 运算符 |
含义 |
示例 |
= != <> |
等于 / 不等于 |
dept_id <> 10 |
> < >= <= |
比较 |
salary >= 8000 |
AND OR NOT |
逻辑 |
a AND (b OR c) |
BETWEEN a AND b |
闭区间 a,b |
salary BETWEEN 8000 AND 15000 |
IN / NOT IN |
属于集合 |
dept_id IN (10,20) |
LIKE |
模糊匹配 |
name LIKE '张%' |
IS NULL / IS NOT NULL |
空值判断 |
dept_id IS NULL |
EXISTS |
子查询是否有结果 |
EXISTS (SELECT 1 FROM ...) |
REGEXP / RLIKE |
正则匹配 |
name REGEXP '^张' |
⚠️ 优先级陷阱 :AND 优先级 > OR。
WHERE a=1 OR b=2 AND c=3 ≡ a=1 OR (b=2 AND c=3)。混用时必须加括号。
5.3 LIKE 与 REGEXP
| 模式 |
含义 |
对 张三丰 |
'张%' |
以张开头 |
✅ |
'%丰' |
以丰结尾 |
✅ |
'张_丰' |
张 + 任意 1 字符 + 丰 |
✅ |
'%三%' |
包含三 |
✅ |
'\_' |
转义下划线本身 |
匹配 _ |
SELECT * FROM employees WHERE name REGEXP '^[张李]'; -- 姓张或李
SELECT * FROM employees WHERE phone REGEXP '^1[3-9][0-9]{9}$'; -- 手机号校验
性能 :'%xx' 前置通配符 → 索引失效 ;'xx%' → 可以走索引。
5.4 NULL 的三值逻辑(★重点)
SELECT * FROM employees WHERE dept_id = NULL; -- ❌ 永远返回空!
SELECT * FROM employees WHERE dept_id IS NULL; -- ✅ 正确
| 表达式 |
结果 |
NULL = NULL |
UNKNOWN(不是 TRUE) |
NULL <> NULL |
UNKNOWN |
NULL AND TRUE |
UNKNOWN |
NULL AND FALSE |
FALSE |
NULL OR TRUE |
TRUE |
NULL OR FALSE |
UNKNOWN |
NOT NULL |
UNKNOWN |
只有 WHERE 结果为 TRUE 的行才返回 ,UNKNOWN 被过滤 → 这就是 = NULL 查不到数据的原因。
另外:COUNT(col)、SUM()、AVG() 都自动忽略 NULL ;COUNT(*) 不忽略。
5.5 ORDER BY 排序
ORDER BY salary DESC, name ASC; -- 多列排序
ORDER BY ISNULL(dept_id), dept_id; -- MySQL 让 NULL 排最后
ORDER BY FIELD(status,'已付款','已退款','待付款'); -- 自定义顺序
| 场景 |
写法 |
| 升序 |
ASC(默认,可省略) |
| 降序 |
DESC |
| NULL 排最后(MySQL) |
ORDER BY col IS NULL, col |
| NULL 排最前(MySQL) |
ORDER BY col IS NOT NULL, col |
| 中文按拼音 |
字段使用 utf8mb4_unicode_ci 排序规则 |
5.6 LIMIT / OFFSET 分页
| 需求 |
写法 |
等价 |
| 前 10 条 |
LIMIT 10 |
LIMIT 0, 10 |
| 第 3 页(每页 10 条) |
LIMIT 10 OFFSET 20 |
LIMIT 20, 10 |
LIMIT a, b = 跳过 a 条、取 b 条,顺序别搞反。
5.7 SQL 逻辑执行顺序(★必背)
书写顺序 ≠ 执行顺序。理解这张表,很多"怪问题"就通了。
| 书写顺序 |
执行顺序 |
阶段 |
关键说明 |
1 SELECT |
6 |
投影 |
选列,别名在此才生效 |
2 FROM |
1 |
取表 |
加载数据源 |
3 JOIN ... ON |
2 |
连接 |
生成中间结果集 |
4 WHERE |
3 |
行过滤 |
不能用聚合、不能用 SELECT 别名 |
5 GROUP BY |
4 |
分组 |
行被压成组 |
6 HAVING |
5 |
组过滤 |
可以用聚合函数 |
7 DISTINCT |
7 |
去重 |
|
8 ORDER BY |
8 |
排序 |
可以用 SELECT 别名 |
9 LIMIT |
9 |
截断 |
最后执行 |
由此推出 3 条铁律:
WHERE 里不能 引用 SELECT 定义的别名(执行时别名还没生成)。
WHERE 里不能用聚合函数(聚合发生在 GROUP BY 之后)。
ORDER BY 可以用别名。
-- ❌ 错:WHERE 里用别名
SELECT salary * 12 AS 年薪 FROM employees WHERE 年薪 > 100000;
-- ✅ 对:重写表达式,或包一层子查询
SELECT * FROM (SELECT salary * 12 AS 年薪 FROM employees) t WHERE 年薪 > 100000;
六、进阶查询 ⭐⭐⭐
6.1 聚合函数
| 函数 |
作用 |
是否忽略 NULL |
COUNT(*) |
统计行数(含全 NULL 行) |
否 |
COUNT(col) |
统计该列非 NULL 值个数 |
是 |
COUNT(DISTINCT col) |
非重复值个数 |
是 |
SUM(col) / AVG(col) |
求和 / 平均 |
是 |
MAX(col) / MIN(col) |
最大 / 最小 |
是 |
GROUP_CONCAT(col) |
组内值拼成字符串 |
是 |
SELECT COUNT(*) AS 总人数,
COUNT(dept_id) AS 有部门人数, -- NULL 不计
AVG(salary) AS 平均薪资,
GROUP_CONCAT(name ORDER BY salary DESC SEPARATOR '、') AS 名单
FROM employees;
-- 名单示例:'王五、李四、赵六、张三'
COUNT(*) vs COUNT(1) vs COUNT(col):前两者都统计行数(现代引擎优化后性能一致);COUNT(col) 跳过 NULL,语义不同。
6.2 GROUP BY / HAVING
SELECT dept_id, COUNT(*) AS 人数, AVG(salary) AS 平均薪资
FROM employees
GROUP BY dept_id
HAVING COUNT(*) >= 2
ORDER BY 平均薪资 DESC;
WHERE vs HAVING 对比(★高频考点)
| 对比项 |
WHERE |
HAVING |
| 执行时机 |
分组前过滤原始行 |
分组后过滤分组结果 |
| 能否用聚合函数 |
❌ |
✅ |
| 能否用 SELECT 别名 |
❌ |
✅(部分引擎支持) |
| 能否用普通列 |
✅ |
只能用 GROUP BY 中出现的列 |
| 性能 |
更优(先缩小数据量) |
较差 |
黄金法则:能放 WHERE 的条件绝不放 HAVING。
GROUP BY 铁律 :SELECT 里出现的非聚合列必须全部出现在 GROUP BY 中 ,否则结果不确定(MySQL 5.7+ 默认开启 ONLY_FULL_GROUP_BY,会直接报错)。
6.3 分组小计:WITH ROLLUP
SELECT dept_id, COUNT(*) AS 人数 FROM employees GROUP BY dept_id WITH ROLLUP;
| dept_id |
人数 |
| 10 |
2 |
| 20 |
1 |
| 30 |
1 |
| NULL |
4 ← 合计行 |
6.4 JOIN 多表连接(★重点)
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;
六种连接对比表
| 类型 |
关键字 |
结果集 |
集合表示 |
| 内连接 |
INNER JOIN / JOIN |
只保留两边都匹配的行 |
A ∩ B |
| 左连接 |
LEFT JOIN |
左表全保留,右表无匹配补 NULL |
A 全部 |
| 右连接 |
RIGHT JOIN |
右表全保留,左表无匹配补 NULL |
B 全部 |
| 全连接 |
FULL OUTER JOIN |
两边全保留,各自无匹配补 NULL |
A ∪ B |
| 交叉连接 |
CROSS JOIN |
笛卡尔积(行数 = M×N) |
A × B |
| 自连接 |
SELF JOIN |
同一张表跟自己连 |
A ⋈ A |
INNER JOIN LEFT JOIN RIGHT JOIN FULL OUTER
┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐
│░░░│ │ │ │███│ │░░░│ │░░░│ │███│ │███│ │░░░│
│ A │ │ B │ │ A │ │ B │ │ A │ │ B │ │ A │ │ B │
└───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘
只取交集 左表全要 右表全要 两边全要
███ = 一定保留的部分 ░░░ = 匹配上才保留 / 补 NULL 保留
MySQL 不支持 FULL OUTER JOIN ,用 LEFT JOIN ... UNION ... RIGHT JOIN 模拟。
自连接示例(查员工及其上级):
SELECT e.name AS 员工, m.name AS 上级
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
ON vs WHERE 在 LEFT JOIN 中的差别(★经典陷阱)
| 写法 |
效果 |
LEFT JOIN b ON a.id=b.id AND b.status=1 |
保留左表全部行,右表只匹配 status=1 的 |
LEFT JOIN b ON a.id=b.id WHERE b.status=1 |
⚠️ 退化成 INNER JOIN(NULL 行被 WHERE 过滤) |
6.5 UNION / UNION ALL
| 关键字 |
去重 |
性能 |
UNION |
✅ 排序去重 |
慢 |
UNION ALL |
❌ 直接拼接 |
快,优先用 |
SELECT name FROM employees
UNION ALL
SELECT dept_name FROM departments; -- 列数、类型需兼容
6.6 子查询
| 类型 |
位置 |
示例 |
| 标量子查询 |
SELECT 列 |
(SELECT AVG(salary) FROM employees) |
| 列子查询 |
WHERE + IN |
WHERE dept_id IN (SELECT dept_id FROM ...) |
| 相关子查询 |
WHERE 引用外层列 |
WHERE salary > (SELECT AVG(salary) FROM employees x WHERE x.dept_id = e.dept_id) |
| 派生表 |
FROM |
FROM (SELECT ... GROUP BY ...) t |
| EXISTS |
WHERE |
WHERE EXISTS (SELECT 1 FROM ... WHERE ...) |
IN vs EXISTS 对比
| 对比 |
IN |
EXISTS |
| 执行方式 |
先算出子查询集合,再判断包含 |
对外层每行执行一次子查询 |
| 适合 |
子查询结果集小 |
子查询结果集大 / 有关联条件 |
| NULL 陷阱 |
⚠️ 子查询含 NULL 时 NOT IN 永远返回空 |
✅ 不受 NULL 影响 |
⚠️ NOT IN + NULL 灾难:
SELECT * FROM employees WHERE dept_id NOT IN (SELECT dept_id FROM departments);
-- 若 departments 中有 dept_id 为 NULL 的行 → 结果永远为空!
-- 因为 x NOT IN (10,20,NULL) 恒为 UNKNOWN
-- ✅ 解决:改用 NOT EXISTS,或子查询加 WHERE dept_id IS NOT NULL
6.7 派生表必须起别名
-- ❌ MySQL 报错:Every derived table must have its own alias
SELECT * FROM (SELECT dept_id FROM employees GROUP BY dept_id);
-- ✅
SELECT * FROM (SELECT dept_id FROM employees GROUP BY dept_id) t;
七、函数大全 ⭐⭐⭐
7.1 字符串函数
| 函数 |
作用 |
示例 → 结果 |
CONCAT(a,b,...) |
拼接(遇 NULL 整体为 NULL) |
CONCAT('A','-','B') → A-B |
CONCAT_WS(sep,a,b) |
带分隔符拼接(跳过 NULL) |
CONCAT_WS('-','A',NULL,'B') → A-B |
SUBSTRING(s,pos,len) |
截取(pos 从 1 开始) |
SUBSTRING('abcdef',2,3) → bcd |
LEFT(s,n) / RIGHT(s,n) |
取左/右 n 个字符 |
LEFT('abcdef',3) → abc |
TRIM / LTRIM / RTRIM |
去空格 |
TRIM(' x ') → x |
UPPER(s) / LOWER(s) |
大小写 |
UPPER('ab') → AB |
REPLACE(s,old,new) |
替换 |
REPLACE('a-b','-','_') → a_b |
LENGTH(s) |
字节长度 |
utf8mb4 下 '中文' → 6 |
CHAR_LENGTH(s) |
字符个数 |
'中文' → 2 |
LOCATE(sub,s) |
子串位置(从 1 起,0 = 没找到) |
LOCATE('b','abc') → 2 |
LPAD(s,n,pad) / RPAD |
左/右补位 |
LPAD('7',3,'0') → 007 |
FIND_IN_SET(s,list) |
在逗号串中找位置 |
FIND_IN_SET('b','a,b,c') → 2 |
⚠️ CONCAT 遇 NULL 结果就是 NULL,要兜底用 IFNULL 或改用 CONCAT_WS。
7.2 数值函数
| 函数 |
作用 |
示例 → 结果 |
ROUND(x,d) |
四舍五入 |
ROUND(3.1415,2) → 3.14 |
TRUNCATE(x,d) |
直接截断 |
TRUNCATE(3.149,2) → 3.14 |
CEIL(x) / FLOOR(x) |
上/下取整 |
CEIL(3.2) → 4 |
ABS(x) |
绝对值 |
|
MOD(a,b) / a % b |
取余 |
MOD(7,3) → 1 |
RAND() |
0~1 随机 |
ORDER BY RAND() LIMIT 1(大表慢) |
GREATEST(a,b,...) / LEAST |
最大 / 最小 |
|
FORMAT(x,d) |
千分位格式化 |
FORMAT(1234567,2) → 1,234,567.00 |
7.3 日期函数
| 函数 |
作用 |
NOW() / CURDATE() / CURTIME() |
当前日期时间 / 日期 / 时间 |
DATE(d) / TIME(d) |
提取日期 / 时间部分 |
YEAR/MONTH/DAY(d) |
取年 / 月 / 日 |
DATE_ADD(d, INTERVAL n unit) |
日期加:DATE_ADD(NOW(), INTERVAL 7 DAY) |
DATE_SUB(d, INTERVAL n unit) |
日期减 |
DATEDIFF(d1,d2) |
相差天数(d1 - d2) |
TIMESTAMPDIFF(unit,d1,d2) |
相差指定单位(YEAR/MONTH/DAY/HOUR...) |
DATE_FORMAT(d,fmt) |
格式化:DATE_FORMAT(NOW(),'%Y-%m-%d') |
STR_TO_DATE(s,fmt) |
字符串转日期 |
LAST_DAY(d) |
当月最后一天 |
DAYOFWEEK(d) |
星期几(1 = 周日) |
UNIX_TIMESTAMP(d) / FROM_UNIXTIME(n) |
时间戳互转 |
常用格式符 :%Y 四位年、%y 两位年、%m 月、%d 日、%H 24 小时、%i 分、%s 秒、%W 星期名。
7.4 条件函数
| 函数 |
作用 |
示例 |
IF(cond,a,b) |
三元表达式 |
IF(salary>10000,'高','低') |
IFNULL(x,default) |
x 为 NULL 返回 default |
IFNULL(dept_id,0) |
COALESCE(a,b,c) |
返回第一个非 NULL |
COALESCE(NULL,NULL,3) → 3 |
NULLIF(a,b) |
a=b 返回 NULL,否则返回 a |
NULLIF(b,0) 防除零 |
CASE WHEN |
多分支 |
见 §8.1 |
SELECT salary / NULLIF(bonus_count, 0) FROM t; -- 安全除法,避免除零报错
7.5 类型转换
SELECT CAST('123' AS UNSIGNED); -- 字符串转数字
SELECT CAST(salary AS CHAR); -- 数字转字符串
SELECT CONVERT('2024-01-01', DATE);
SELECT '123' + 0; -- 隐式转数字(小技巧)
| 目标类型 |
写法 |
| 整数 |
SIGNED / UNSIGNED |
| 小数 |
DECIMAL(10,2) |
| 字符串 |
CHAR / CHAR(20) |
| 日期 |
DATE / DATETIME / TIME |
| 二进制 |
BINARY |
7.6 JSON 函数
SELECT JSON_OBJECT('id',1,'name','张三'); -- {"id": 1, "name": "张三"}
SELECT JSON_ARRAY(1,2,3); -- [1, 2, 3]
SELECT JSON_EXTRACT('{"a":1}', '$.a'); -- 1
SELECT JSON_SET('{"a":1}', '$.b', 2); -- {"a": 1, "b": 2}
SELECT JSON_LENGTH('{"a":1,"b":2}'); -- 2
SELECT JSON_VALID('{"a":1}'); -- 1
7.7 系统 / 元信息函数
| 函数 |
作用 |
DATABASE() |
当前数据库名 |
VERSION() |
数据库版本 |
USER() / CURRENT_USER() |
当前用户 |
LAST_INSERT_ID() |
最近插入的自增 ID |
ROW_COUNT() |
上条 DML 影响的行数 |
FOUND_ROWS() |
配合 SQL_CALC_FOUND_ROWS 取总数 |
八、高级特性 ⭐⭐⭐⭐
8.1 CASE WHEN ------ SQL 里的 if-else
SELECT name, salary,
CASE
WHEN salary >= 15000 THEN '高'
WHEN salary >= 10000 THEN '中'
ELSE '低'
END AS 薪资档位
FROM employees;
| 两种写法 |
语法 |
适用 |
| 简单 CASE |
CASE 列 WHEN 值1 THEN r1 WHEN 值2 THEN r2 END |
只有等值判断 |
| 搜索 CASE |
CASE WHEN 条件1 THEN r1 WHEN 条件2 THEN r2 END |
任意条件(更灵活) |
⚠️ 忘记写 ELSE 时,不满足任何分支返回 NULL。
实战:行转列(透视表)
SELECT dept_id,
SUM(CASE WHEN salary >= 15000 THEN 1 ELSE 0 END) AS 高薪人数,
SUM(CASE WHEN salary < 15000 THEN 1 ELSE 0 END) AS 普通人数,
SUM(CASE WHEN hire_date >= '2022-01-01' THEN 1 ELSE 0 END) AS 新人
FROM employees
GROUP BY dept_id;
8.2 窗口函数 ------ 高级 SQL 的分水岭 ⭐⭐⭐⭐
基本语法
函数名() OVER (
[PARTITION BY 分组列] -- 类似 GROUP BY,但不合并行
[ORDER BY 排序列 [ASC|DESC]] -- 决定计算顺序
[ROWS|RANGE BETWEEN ... AND ...] -- 滑动窗口范围
)
窗口函数 vs GROUP BY(★核心区别)
| 对比 |
GROUP BY |
窗口函数 |
| 输出行数 |
压缩(每组 1 行) |
不变(每行都保留) |
| 结果位置 |
直接作为列值 |
作为附加列挂在原行旁边 |
| 能否保留明细 |
❌ |
✅ |
SELECT dept_id, AVG(salary) FROM employees GROUP BY dept_id; -- 3 行
SELECT name, dept_id, salary,
AVG(salary) OVER (PARTITION BY dept_id) AS 本部门平均
FROM employees; -- 5 行,明细都在
排名三兄弟对比(★超高频考点)
| 函数 |
并列时行为 |
序号是否跳跃 |
值为 100,100,90 时 |
ROW_NUMBER() |
并列也强制不同号 |
不跳 |
1, 2, 3 |
RANK() |
并列同号 |
跳跃 |
1, 1, 3 |
DENSE_RANK() |
并列同号 |
不跳 |
1, 1, 2 |
NTILE(n) |
分 n 桶 |
--- |
分 2 桶:1, 1, 2 |
SELECT name, dept_id, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drk
FROM employees;
取"每组唯一 Top1"用 ROW_NUMBER();"并列第一都要"用 RANK() / DENSE_RANK()。
其他常用窗口函数
| 函数 |
作用 |
典型用途 |
SUM/AVG/COUNT/MAX/MIN() OVER(...) |
组内聚合 |
累计值、占比、组内基准 |
LAG(col, n, default) |
取前 n 行的值 |
环比、上期对比 |
LEAD(col, n, default) |
取后 n 行的值 |
下期、留存 |
FIRST_VALUE(col) |
组内第一行的值 |
基准值 |
LAST_VALUE(col) |
组内最后一行的值(注意 frame 默认到当前行) |
|
NTH_VALUE(col, n) |
组内第 n 行的值 |
|
NTILE(n) |
分成 n 桶 |
分位数、Top N% |
PERCENT_RANK() |
相对排名 (rank-1)/(rows-1) |
|
CUME_DIST() |
累计分布 |
|
-- 环比
SELECT order_id, amount,
LAG(amount, 1) OVER (ORDER BY order_date) AS 上单金额,
amount - LAG(amount, 1) OVER (ORDER BY order_date) AS 差值
FROM orders;
-- 累计求和
SELECT order_id, amount,
SUM(amount) OVER (ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS 累计
FROM orders;
-- 3 笔移动平均
SELECT order_date, amount,
AVG(amount) OVER (ORDER BY order_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS 移动平均
FROM orders;
-- 组内占比
SELECT name, dept_id, salary,
ROUND(salary / SUM(salary) OVER (PARTITION BY dept_id) * 100, 2) AS 部门占比
FROM employees;
窗口范围(Frame)对比表(★难理解,重点看)
| 窗口定义 |
含义 |
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
组开头 ~ 当前行(带 ORDER BY 时的默认) |
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
整个分组(不带 ORDER BY 时的默认) |
ROWS BETWEEN n PRECEDING AND CURRENT ROW |
前 n 行 ~ 当前行(滑动平均) |
ROWS BETWEEN n PRECEDING AND m FOLLOWING |
前 n 行 ~ 后 m 行 |
RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW |
按值范围(近 7 天),不是按行数 |
| 关键词 |
含义 |
UNBOUNDED PRECEDING |
分组第一行 |
UNBOUNDED FOLLOWING |
分组最后一行 |
n PRECEDING |
当前行往前 n 行 |
n FOLLOWING |
当前行往后 n 行 |
CURRENT ROW |
当前行 |
ROWS vs RANGE 区别:
|
ROWS |
RANGE |
| 单位 |
物理行 |
逻辑值 |
| 并列值处理 |
每行单独算 |
同值行视为一组一起算 |
| 示例 |
ROWS 1 PRECEDING = 恰好前 1 行 |
RANGE 1 PRECEDING = 值 ≤ 当前值 -1 的所有行 |
命名窗口(复用定义)
SELECT name, salary,
ROW_NUMBER() OVER w AS rn,
RANK() OVER w AS rk,
SUM(salary) OVER w AS cum
FROM employees
WINDOW w AS (PARTITION BY dept_id ORDER BY salary DESC);
⚠️ 窗口函数在 SELECT 阶段执行,晚于 WHERE / GROUP BY / HAVING 。
所以 WHERE ROW_NUMBER() OVER(...) = 1 是非法的,必须子查询或 CTE 包一层。
8.3 CTE(WITH 子句)⭐⭐⭐⭐
WITH dept_stat AS (
SELECT dept_id, AVG(salary) AS avg_sal
FROM employees GROUP BY dept_id
)
SELECT e.name, e.salary, d.avg_sal
FROM employees e JOIN dept_stat d ON e.dept_id = d.dept_id
WHERE e.salary > d.avg_sal;
CTE vs 子查询 / 临时表
| 对比 |
CTE |
嵌套子查询 |
临时表 |
| 可读性 |
高(自顶向下) |
低(层层嵌套) |
中 |
| 可复用 |
✅ 定义一次多处引用 |
❌ 需重复写 |
✅ |
| 递归 |
✅ WITH RECURSIVE |
❌ |
❌ |
| 是否落盘 |
否(多数引擎内联优化) |
否 |
✅ 落盘 |
| 作用域 |
单条语句 |
单条语句 |
整个会话 |
递归 CTE(树形结构、层级、日期序列生成):
WITH RECURSIVE org AS (
SELECT emp_id, name, manager_id, 1 AS lvl
FROM employees WHERE manager_id IS NULL -- 锚点:根节点
UNION ALL
SELECT e.emp_id, e.name, e.manager_id, o.lvl + 1
FROM employees e JOIN org o ON e.manager_id = o.emp_id -- 递归:下级
)
SELECT * FROM org ORDER BY lvl;
-- 生成连续日期(报表补零用)
WITH RECURSIVE dates AS (
SELECT DATE('2024-01-01') AS d
UNION ALL
SELECT d + INTERVAL 1 DAY FROM dates WHERE d < '2024-01-31'
)
SELECT * FROM dates;
8.4 视图与临时表
| 对象 |
语法 |
特点 |
| 视图 |
CREATE VIEW v AS SELECT ... |
虚拟表,不存数据,查询时实时计算 |
| 物化视图 |
CREATE MATERIALIZED VIEW(MySQL 用表 + 定时任务模拟) |
存结果,需刷新 |
| 临时表 |
CREATE TEMPORARY TABLE |
会话级,断开自动消失,仅当前连接可见 |
| 派生表 |
FROM (SELECT ...) t |
语句级 |
CREATE OR REPLACE VIEW v_high_salary AS
SELECT name, dept_id, salary FROM employees WHERE salary > 10000;
SELECT * FROM v_high_salary; -- 像普通表一样查
DROP VIEW v_high_salary;
| 视图优点 |
视图缺点 |
| 简化复杂查询、隐藏表结构 |
不存数据,复杂视图查询慢 |
| 权限控制(只给视图权限) |
嵌套视图易形成性能黑洞 |
| 逻辑复用 |
含聚合/DISTINCT/UNION 的视图不可更新 |
8.5 预处理语句(防 SQL 注入)⭐⭐⭐
PREPARE stmt FROM 'SELECT * FROM employees WHERE dept_id = ?';
SET @dept = 10;
EXECUTE stmt USING @dept;
DEALLOCATE PREPARE stmt;
| 对比 |
字符串拼接 |
参数化(预处理) |
| SQL 注入风险 |
⚠️ 极高 |
✅ 无 |
| 解析开销 |
每次硬解析 |
一次解析多次执行 |
| 反例 |
'... WHERE id = ' + userId |
经典注入:' OR '1'='1 |
永远不要把用户输入拼进 SQL。 应用层应使用 PreparedStatement / ORM 的参数绑定。
8.6 慢查询排查
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SHOW PROCESSLIST; -- 当前连接 / 正在跑的 SQL
KILL 12345; -- 杀掉慢查询
| 排查步骤 |
命令 |
| 找出慢 SQL |
慢查询日志 / SHOW PROCESSLIST |
| 看执行计划 |
EXPLAIN / EXPLAIN ANALYZE |
| 看索引是否生效 |
关注 key、type |
| 看索引/统计信息 |
SHOW INDEX FROM t、ANALYZE TABLE t |
| 看锁等待 |
SHOW ENGINE INNODB STATUS |
九、索引与执行计划 ⭐⭐⭐⭐⭐
9.1 索引数据结构对比
| 结构 |
支持情况 |
特点 |
适用 |
| B+Tree |
InnoDB / MyISAM 默认 |
有序、支持范围查询、树高低 |
绝大多数场景 |
| Hash |
Memory / InnoDB 自适应哈希 |
O(1) 等值,不支持范围/排序 |
纯等值查询 |
| Full-Text |
InnoDB 5.6+ |
倒排索引 |
文本搜索 |
| R-Tree |
MyISAM / InnoDB |
空间索引 |
地理坐标 |
为什么是 B+Tree?
- 二叉搜索树:树太高,IO 次数多;
- B-Tree:非叶子节点也存数据,单页能放的索引项少 → 树更高;
- B+Tree :非叶子只存键值(一页可放上千个索引项,树高仅 3~4 层),且叶子节点用链表相连,范围查询极快。
9.2 聚簇索引 vs 二级索引(★重点)
| 对比 |
聚簇索引(主键) |
二级索引(辅助索引) |
| 数量 |
每表 1 个 |
可多个 |
| 叶子存什么 |
整行数据 |
索引列 + 主键值 |
| 查到后 |
直接拿到数据 |
需回表:拿主键再查聚簇索引 |
| 没有主键时 |
InnoDB 选第一个非空唯一索引;再没有则生成 6 字节隐藏 rowid |
|
二级索引 idx(dept_id) 聚簇索引 PRIMARY(emp_id)
┌─────────┬────────┐ ┌────────┬──────────────────────┐
│ dept_id │ emp_id │ --回表--> │ emp_id │ name salary ... 整行 │
├─────────┼────────┤ ├────────┼──────────────────────┤
│ 10 │ 1 │ │ 1 │ 张三 8000 ... │
│ 10 │ 2 │ │ 2 │ 李四 12000 ... │
│ 20 │ 3 │ │ 3 │ 王五 15000 ... │
└─────────┴────────┘ └────────┴──────────────────────┘
回表与覆盖索引
| 概念 |
说明 |
| 回表 |
走二级索引拿到主键后,再去聚簇索引取整行 → 多一次 IO |
| 覆盖索引 |
查询的列全都在索引里 ,无需回表 → EXPLAIN 的 Extra 显示 Using index |
-- 假设有索引 idx(dept_id, salary)
SELECT dept_id, salary FROM employees WHERE dept_id = 10;
-- ✅ 覆盖索引,不用回表
SELECT name, salary FROM employees WHERE dept_id = 10;
-- ❌ name 不在索引里 → 必须回表
优化手段:把查询高频的列加进联合索引,做成覆盖索引。
9.3 索引类型
| 类型 |
关键字 |
特点 |
| 主键索引 |
PRIMARY KEY |
唯一、非空、聚簇 |
| 唯一索引 |
UNIQUE |
值唯一,允许多个 NULL |
| 普通索引 |
INDEX / KEY |
最常用 |
| 前缀索引 |
INDEX(col(n)) |
只索引前 n 字符,省空间(牺牲区分度) |
| 联合索引 |
INDEX(a,b,c) |
多列组合,遵守最左前缀 |
| 全文索引 |
FULLTEXT |
MATCH(col) AGAINST('关键词') |
9.4 最左前缀原则(★联合索引核心)
有联合索引 idx(a, b, c),相当于同时拥有 (a)、(a,b)、(a,b,c) 三个索引:
| 查询条件 |
是否走索引 |
用到哪些列 |
WHERE a=? |
✅ |
a |
WHERE a=? AND b=? |
✅ |
a, b |
WHERE a=? AND b=? AND c=? |
✅ |
a, b, c(完全命中) |
WHERE a=? AND c=? |
⚠️ |
只用 a(跳过 b,c 用不上) |
WHERE b=? AND c=? |
❌ |
无(跳过 a,索引失效) |
WHERE b=? AND a=? |
✅ |
a, b(优化器会自动重排顺序) |
WHERE a>? AND b=? |
⚠️ |
只用 a(范围条件后的列失效) |
两条关键结论:
- 联合索引要从最左列开始连续匹配;
- 遇到范围查询 (
>、<、BETWEEN、前缀 LIKE)后,其右边的列不再走索引。
9.5 索引失效场景表(★必背)
| 场景 |
反例 |
正解 |
| 对索引列用函数 |
WHERE YEAR(created_at)=2024 |
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01' |
| 隐式类型转换 |
WHERE phone = 13800138000(phone 是 VARCHAR) |
WHERE phone = '13800138000' |
| 前置通配符 |
WHERE name LIKE '%张' |
改用全文索引 / 冗余反转列 |
| OR 连接非索引列 |
WHERE a=1 OR b=2(b 无索引) |
分别建索引 / 改 UNION ALL |
!= / <> / NOT IN |
WHERE status != 1 |
改写成正向范围 |
| 索引列参与运算 |
WHERE salary + 100 > 5000 |
WHERE salary > 4900 |
| JOIN 时字符集不一致 |
utf8mb4 JOIN utf8 |
统一字符集 |
| 区分度太低 |
WHERE gender='男'(90% 都是) |
优化器直接全表,别建 |
| 数据分布倾斜 |
某值占比过高 |
同上 |
9.6 EXPLAIN 详解(★重点)
EXPLAIN SELECT * FROM employees WHERE dept_id = 10;
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0.18+,给出真实耗时
type 访问类型(从好到坏)
| type |
含义 |
评价 |
system |
表只有一行 |
最好 |
const |
主键/唯一索引命中一行 |
极好 |
eq_ref |
JOIN 时主键/唯一索引匹配 |
极好 |
ref |
普通索引等值匹配 |
好 |
range |
索引范围扫描 |
好 |
index |
全索引扫描(扫整棵索引树) |
一般 |
ALL |
全表扫描 |
⚠️ 必须优化 |
关键列说明表
| 列 |
含义 |
关注点 |
id |
查询序号 |
越大越先执行;相同则从上往下 |
select_type |
查询类型 |
SIMPLE / PRIMARY / SUBQUERY / DERIVED / UNION |
table |
表名 |
<derived2> 表示派生表 |
type |
访问类型 |
见上表 |
possible_keys |
可能用到的索引 |
|
key |
实际使用的索引 |
NULL = 没走索引 |
key_len |
使用的索引字节数 |
可判断联合索引用了几列 |
ref |
与索引比较的列/常量 |
|
rows |
预估扫描行数 |
越小越好 |
filtered |
过滤后剩余百分比 |
|
Extra |
额外信息 |
见下表 |
| Extra |
含义 |
评价 |
Using index |
覆盖索引,无需回表 |
✅ 好 |
Using where |
存储引擎返回后还要过滤 |
正常 |
Using index condition |
索引下推(ICP) |
✅ 好 |
Using filesort |
需要额外排序 |
⚠️ 考虑加索引消除排序 |
Using temporary |
用了临时表(GROUP BY / DISTINCT) |
⚠️ 需优化 |
Using join buffer |
JOIN 用了缓冲区(BNL) |
⚠️ 加索引 |
Using union(idx1,idx2) |
索引合并 |
一般 |
9.7 索引设计原则
| 原则 |
说明 |
建在 WHERE/ORDER BY/GROUP BY/JOIN 的列上 |
只查不筛的列不必建 |
| 区分度高的列优先 |
COUNT(DISTINCT col)/COUNT(*) 越接近 1 越好 |
| 联合索引顺序:等值列在前,范围列在后 |
WHERE a=1 AND b>2 → idx(a,b) |
| 考虑覆盖索引 |
把 SELECT 的列纳入索引 |
| 前缀索引省空间 |
长字符串取前 N 字符,保证区分度 |
| 索引不是越多越好 |
每个索引都是一棵 B+Tree,拖慢写入、占空间 |
| 单表索引建议 ≤ 5~6 个 |
|
| 优先扩展已有索引,别新建 |
idx(a) → idx(a,b) |
| 避免冗余索引 |
有了 (a,b) 就不需要单独的 (a) |
9.8 高级优化特性(了解)
| 特性 |
说明 |
| ICP 索引下推 |
5.6+,把 WHERE 过滤下推到存储引擎层,减少回表 |
| MRR |
把随机 IO 转成顺序 IO(先排序主键再回表) |
| 索引合并 |
多个索引的 OR/AND 结果合并(Using union) |
| 自适应哈希 |
InnoDB 自动为热点页建哈希索引 |
十、事务与锁 ⭐⭐⭐⭐⭐
10.1 ACID
| 特性 |
含义 |
实现机制 |
| A 原子性 Atomicity |
要么全成功,要么全回滚 |
undo log |
| C 一致性 Consistency |
事务前后完整性约束不变 |
约束 + A/I/D 共同保证 |
| I 隔离性 Isolation |
并发事务互不干扰 |
锁 + MVCC |
| D 持久性 Durability |
提交后永久保存 |
redo log |
BEGIN; -- 或 START TRANSACTION
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 出错则 ROLLBACK
SAVEPOINT sp1; -- 保存点
ROLLBACK TO sp1; -- 回滚到保存点(不是全部回滚)
10.2 四种隔离级别(★必背)
| 级别 |
脏读 |
不可重复读 |
幻读 |
实现 |
| 读未提交 Read Uncommitted |
✅ 有 |
✅ 有 |
✅ 有 |
无锁(几乎不用) |
| 读已提交 Read Committed |
❌ |
✅ 有 |
✅ 有 |
每条 SQL 生成新 ReadView |
| 可重复读 Repeatable Read(MySQL 默认) |
❌ |
❌ |
⚠️ 基本解决 |
事务首次快照读生成 ReadView + 间隙锁 |
| 串行化 Serializable |
❌ |
❌ |
❌ |
全部加锁,串行执行 |
SELECT @@transaction_isolation; -- 查看隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 设置
三种并发问题
| 问题 |
现象 |
侧重 |
| 脏读 |
读到别人未提交的数据;对方回滚后数据就是假的 |
|
| 不可重复读 |
同一事务内两次读同一行,值不同 |
侧重 UPDATE / DELETE |
| 幻读 |
同一事务内两次范围查询,行数不同 |
侧重 INSERT(多出"幻影行") |
10.3 MVCC 多版本并发控制(★难点)
| 概念 |
说明 |
| 隐藏列 |
每行有 DB_TRX_ID(最后修改的事务 ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID |
| undo log 版本链 |
每次修改把旧值串成链表,可沿链找回历史版本 |
| ReadView |
快照读时生成,记录"当前活跃事务列表",决定哪个版本可见 |
| 快照读 |
普通 SELECT,不加锁,读历史版本 |
| 当前读 |
SELECT ... FOR UPDATE、LOCK IN SHARE MODE、INSERT/UPDATE/DELETE,读最新版本并加锁 |
| 隔离级别 |
ReadView 生成时机 |
效果 |
| RC |
每条 SQL 语句都生成新的 |
每次看到最新已提交数据 → 会不可重复读 |
| RR |
事务中第一次快照读时生成,之后复用 |
整个事务看到同一版本 → 可重复读 |
版本链示意(undo log)
当前行 --roll_ptr--> 版本2 --roll_ptr--> 版本1 --> (最初)
trx_id=30 trx_id=20 trx_id=10
10.4 锁(★重点)
按粒度分
| 锁 |
粒度 |
并发 |
开销 |
引擎 |
| 表锁 |
整表 |
低 |
小 |
MyISAM、InnoDB(DDL 时) |
| 行锁 |
单行 |
高 |
大 |
InnoDB |
| 页锁 |
一页 |
中 |
中 |
BDB |
按类型分
| 锁 |
全称 |
兼容性 |
加锁方式 |
| 共享锁 S |
Shared Lock(读锁) |
多个 S 可共存,S 与 X 互斥 |
SELECT ... LOCK IN SHARE MODE |
| 排他锁 X |
Exclusive Lock(写锁) |
与任何锁互斥 |
SELECT ... FOR UPDATE / UPDATE / DELETE |
| 意向共享 IS |
表级,声明"我要加行级 S 锁" |
|
自动 |
| 意向排他 IX |
表级,声明"我要加行级 X 锁" |
|
自动 |
InnoDB 行锁三种算法(★难点)
| 锁类型 |
锁定范围 |
解决的问题 |
| Record Lock 记录锁 |
锁定单条索引记录 |
防止该行被修改 |
| Gap Lock 间隙锁 |
锁定两条记录之间的间隙(不含记录本身) |
防止插入 → 解决幻读 |
| Next-Key Lock 临键锁 |
Record + Gap ,左开右闭 (a, b] |
InnoDB 默认行锁算法 |
索引值: 10 20 30
间隙: (-∞,10) (10,20) (20,30) (30,+∞)
Next-Key: (-∞,10] (10,20] (20,30] (30,+∞]
注意 :间隙锁只在 RR 及以上隔离级别生效(RC 下没有)。
锁升级为表锁的情况
| 场景 |
原因 |
不走索引的 UPDATE / DELETE |
InnoDB 只能锁住扫描到的所有行 ≈ 锁全表 |
| 索引失效 |
同上 |
⚠️ UPDATE t SET x=1 WHERE name='张三' 若 name 无索引 → 锁全表! 线上最常见事故之一。
10.5 死锁
| 项目 |
说明 |
| 产生条件 |
互斥、占有且等待、不可抢占、循环等待 |
| InnoDB 处理 |
检测死锁后回滚代价较小的事务 (wait-for graph),或等 innodb_lock_wait_timeout(默认 50s)超时 |
| 排查 |
SHOW ENGINE INNODB STATUS 看 LATEST DETECTED DEADLOCK 段 |
| 避免手段 |
① 固定加锁顺序;② 缩小事务、尽快提交;③ 走索引减少锁范围;④ 避免大事务;⑤ 必要时降级到 RC |
10.6 三大日志与两阶段提交
| 日志 |
层级 |
作用 |
| redo log |
InnoDB 引擎层,物理日志 |
崩溃恢复,保证 D(持久性) |
| undo log |
InnoDB 引擎层,逻辑日志 |
回滚 + MVCC 版本链,保证 A(原子性) |
| binlog |
MySQL Server 层,逻辑日志 |
主从复制、数据恢复、审计 |
写入流程(WAL:Write-Ahead Logging)
UPDATE → 修改内存 Buffer Pool
→ 写 redo log(prepare 状态)
→ 写 binlog
→ redo log 标记 commit ← 两阶段提交,保证 redo 与 binlog 一致
→ 后台线程异步刷脏页
为什么需要两阶段提交:防止 redo log 写成功但 binlog 写失败,导致主库(redo)与从库(binlog)数据不一致。
十一、数据库设计基础 ⭐⭐⭐
11.1 三范式
| 范式 |
要求 |
反例 → 正解 |
| 1NF |
列不可再分(原子性) |
address='北京市朝阳区xx路' 存一整串 → 拆成省/市/区/详细地址 |
| 2NF |
在 1NF 基础上,非主键列必须完全依赖主键(消除部分依赖) |
订单明细 (订单ID,商品ID) → 商品名称,商品名称只依赖商品ID → 拆出商品表 |
| 3NF |
在 2NF 基础上,非主键列不能传递依赖 |
员工表有 部门ID → 部门名称 → 部门位置 → 拆出部门表 |
| BCNF |
3NF 基础上,主属性也不能部分/传递依赖于码 |
更严格的 3NF |
口诀:1NF 原子、2NF 完全依赖、3NF 直接依赖(不传递)。
11.2 范式 vs 反范式
| 项目 |
范式化(拆表) |
反范式化(冗余) |
| 数据冗余 |
少 |
多 |
| 一致性 |
容易保证 |
难(要靠程序/触发器同步) |
| 查询 |
需 JOIN,慢 |
少 JOIN,快 |
| 写入 |
快、无异常 |
慢(要更新多处) |
| 适用 |
OLTP(业务系统) |
OLAP(报表、数仓) |
实践 :核心业务表做到 3NF;报表/查询热点做适度冗余 (如订单表冗余 user_name 快照)。
11.3 主键选型对比
| 方案 |
优点 |
缺点 |
适用 |
AUTO_INCREMENT 自增 |
顺序写入、索引紧凑、性能最好 |
分库分表需额外处理;易泄露业务量 |
单体首选 |
| UUID |
全局唯一、可提前生成 |
36 字节太长、随机写入导致页分裂、索引膨胀 |
一般不用 |
UUID 存 BINARY(16) |
省空间(16 字节) |
仍是无序写入 |
折中 |
| 雪花算法 Snowflake |
全局唯一、趋势递增、8 字节 |
需发号器、依赖时钟 |
分布式首选 |
11.4 表设计检查清单
| # |
检查项 |
| 1 |
每张表都有主键,且是 BIGINT / INT |
| 2 |
字符集统一 utf8mb4,排序规则统一 |
| 3 |
所有字段尽量 NOT NULL + 合理 DEFAULT |
| 4 |
金额一律 DECIMAL,不用 FLOAT |
| 5 |
时间统一 DATETIME,不要用字符串存时间 |
| 6 |
表/字段有 COMMENT 注释 |
| 7 |
外键是否使用(大厂多数禁用,改由应用层保证) |
| 8 |
必备字段:created_at、updated_at、is_deleted |
| 9 |
长文本(TEXT)是否垂直拆分 |
| 10 |
高频查询条件是否有对应索引 |
十二、易错点与调优清单 ⭐⭐⭐
12.1 高频踩坑表
| 坑 |
错误写法 |
正确写法 |
| NULL 判断 |
WHERE x = NULL |
WHERE x IS NULL |
| NOT IN 遇 NULL |
WHERE id NOT IN (SELECT...) |
用 NOT EXISTS 或子查询加 IS NOT NULL |
| 全表更新 |
UPDATE t SET x=1; |
必须带 WHERE |
| 除零 |
a / b |
a / NULLIF(b, 0) |
| 浮点比较 |
WHERE price = 0.1 |
ABS(price-0.1) < 1e-6 或改用 DECIMAL |
| COUNT 语义 |
COUNT(col) 当行数 |
行数用 COUNT(*) |
| 深分页 |
LIMIT 1000000, 20 |
游标:WHERE id > 上页最大id LIMIT 20 |
| GROUP BY 漏列 |
SELECT dept,name,COUNT(*) GROUP BY dept |
非聚合列全部进 GROUP BY |
| LEFT JOIN 退化 |
LEFT JOIN ... WHERE 右表列=值 |
条件写在 ON 里 |
| WHERE 用别名 |
SELECT a+b AS s ... WHERE s>1 |
重写表达式或包子查询 |
| 窗口函数过滤 |
WHERE ROW_NUMBER() OVER(...)=1 |
子查询/CTE 包一层 |
| 字符串日期 |
WHERE dt='2024/1/1' |
统一 '2024-01-01' |
| 隐式转换 |
WHERE varchar_col = 123 |
WHERE varchar_col = '123' |
| 大事务 |
一次 UPDATE 百万行 |
分批 + LIMIT |
| 中文乱码 / emoji 存不进 |
用了 utf8 |
必须 utf8mb4 |
12.2 SQL 优化清单
| # |
优化点 |
| 1 |
不用 SELECT *,只查需要的列(利于覆盖索引) |
| 2 |
WHERE 条件列建索引,遵守最左前缀 |
| 3 |
避免索引列上使用函数 / 运算 / 隐式转换 |
| 4 |
LIKE 避免前置 %,长文本用全文索引 |
| 5 |
UNION ALL 替代 UNION(能确定无重复时) |
| 6 |
小表驱动大表(JOIN 时小表放前面) |
| 7 |
深分页用游标(记录上次最大 ID) |
| 8 |
大批量操作分批处理,避免长事务、大锁 |
| 9 |
子查询结果大时用 EXISTS 替代 IN |
| 10 |
避免 !=、OR(改 UNION ALL) |
| 11 |
ORDER BY 字段建索引,消除 filesort |
| 12 |
GROUP BY 字段建索引,消除临时表 |
| 13 |
合理冗余,减少 JOIN |
| 14 |
大表 ALTER 用 pt-osc / gh-ost |
| 15 |
定期 ANALYZE TABLE 更新统计信息 |
| 16 |
冷热数据分离,归档历史数据 |
| 17 |
读写分离(主写从读) |
| 18 |
分库分表(单表超千万行 / 2GB 时考虑) |
| 19 |
连接池参数合理(避免连接数打满) |
| 20 |
上线前先 EXPLAIN |
12.3 深分页优化对比
-- ❌ 慢:扫描并丢弃前 100 万行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 方案1:游标(记住上一页最后一条 id)------ 最快
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
-- ✅ 方案2:延迟关联(覆盖索引先查 id,再回表)
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;
-- ✅ 方案3:业务上限制只能翻前 100 页
| 方案 |
适用 |
优点 |
缺点 |
| 游标 |
可连续翻页(信息流) |
最快 |
不能跳页 |
| 延迟关联 |
必须支持跳页 |
比直接 LIMIT 快很多 |
仍需扫描索引 |
| 限制页数 |
搜索类 |
简单 |
体验受限 |
12.4 扩展:分库分表(了解)
| 概念 |
说明 |
| 垂直拆分 |
按业务 拆库(用户库、订单库);按字段拆表(大字段独立) |
| 水平拆分 |
按规则 把数据分到多张同构表(如 user_id % 8) |
| 分片键 |
拆分依据的字段,选最常用于查询条件的字段 |
| 中间件 |
ShardingSphere、MyCat |
| 带来的问题 |
分布式事务、跨库 JOIN、全局 ID、扩容迁移 |
十三、SQL 方言对比 ⭐⭐
| 能力 |
MySQL |
PostgreSQL |
Oracle |
SQL Server |
| 取前 N 行 |
LIMIT n |
LIMIT n |
ROWNUM <= n / FETCH FIRST n ROWS ONLY |
TOP n |
| 分页 |
LIMIT n OFFSET m |
LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY |
OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| 字符串拼接 |
CONCAT(a,b) |
`a |
|
b` |
| 自增列 |
AUTO_INCREMENT |
SERIAL / IDENTITY |
SEQUENCE + 触发器 |
IDENTITY(1,1) |
| 当前时间 |
NOW() |
NOW() |
SYSDATE |
GETDATE() |
| 空值兜底 |
IFNULL(a,b) |
COALESCE(a,b) |
NVL(a,b) |
ISNULL(a,b) |
| 截取字符串 |
SUBSTRING(s,1,3) |
SUBSTRING(s FROM 1 FOR 3) |
SUBSTR(s,1,3) |
SUBSTRING(s,1,3) |
| 日期格式化 |
DATE_FORMAT(d,'%Y-%m-%d') |
TO_CHAR(d,'YYYY-MM-DD') |
TO_CHAR(d,'YYYY-MM-DD') |
FORMAT(d,'yyyy-MM-dd') |
| 字符串转日期 |
STR_TO_DATE(s,f) |
TO_DATE(s,'YYYY-MM-DD') |
TO_DATE(s,'YYYY-MM-DD') |
CONVERT(DATE,s) |
| 日期差 |
DATEDIFF(d1,d2) |
d1 - d2(整数天) |
d1 - d2 |
DATEDIFF(day,d1,d2) |
| FULL OUTER JOIN |
❌ 不支持(需 UNION 模拟) |
✅ |
✅ |
✅ |
| 窗口函数 |
8.0+ |
✅ 很早支持 |
✅ |
✅ |
| CTE / 递归 CTE |
8.0+ |
✅ |
✅(另有 CONNECT BY) |
✅ |
| 布尔类型 |
TINYINT(1) |
BOOLEAN |
无(用 NUMBER(1)) |
BIT |
| 标识符引用符 |
反引号 ````` |
双引号 " |
双引号 " |
方括号 [] |
| 分组内拼接 |
GROUP_CONCAT |
STRING_AGG |
LISTAGG |
STRING_AGG |
| 正则 |
REGEXP |
~ / SIMILAR TO |
REGEXP_LIKE |
LIKE(有限) |
迁移提醒 :重点关注分页、字符串拼接、日期函数、标识符引用符这四类,改不完会直接报错。
十四、面试高频题 ⭐⭐⭐⭐
| 题目 |
核心考点 |
思路 |
| 查各部门薪资最高的人 |
窗口函数 / 相关子查询 |
ROW_NUMBER() OVER(PARTITION BY dept_id ORDER BY salary DESC) 取 rn=1 |
| 第 N 高的薪水(去重) |
DENSE_RANK / LIMIT+OFFSET |
注意并列,用 DENSE_RANK()=N 更稳 |
| 查连续 3 天登录的用户 |
窗口 + 日期差 |
ROW_NUMBER() 与日期做差,差值相同即连续 |
| 删除重复数据只留一条 |
窗口 + 自连接 |
按业务键分区,ROW_NUMBER()>1 的删掉 |
| 次日留存率 |
自连接 + DATEDIFF |
今日活跃 JOIN 次日活跃,相除 |
| 行转列 / 列转行 |
CASE WHEN / UNION ALL |
行→列用聚合 + CASE;列→行用 UNION ALL |
| 找出连续区间的起止 |
窗口 + 差值分组 |
id - ROW_NUMBER() 作为分组标识 |
| 累计 / 环比 / 同比 |
窗口函数 |
SUM() OVER / LAG() |
| 求中位数 |
窗口 + 计数 |
正排 + 倒排取中间,或用 PERCENT_RANK |
| 组内占比 / Top N% |
窗口函数 |
x / SUM(x) OVER(PARTITION BY ...) |
| 每个用户最近一笔订单 |
窗口 + 过滤 |
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY date DESC)=1 |
| 树形结构(部门层级) |
递归 CTE |
WITH RECURSIVE |
大表 COUNT(*) 慢 |
索引 / 汇总表 / 近似值 |
二级索引计数、维护汇总表、SHOW TABLE STATUS 估算 |
| 这条 SQL 为什么没走索引 |
EXPLAIN |
见 §9.5 失效场景 |
例题 1:各部门薪资最高的员工(并列都要)
WITH ranked AS (
SELECT name, dept_id, salary,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drk
FROM employees
)
SELECT name, dept_id, salary FROM ranked WHERE drk = 1;
例题 2:连续登录 ≥ 3 天的用户
WITH t AS (
SELECT DISTINCT user_id, login_date FROM login_log
), g AS (
SELECT user_id, login_date,
DATE_SUB(login_date, INTERVAL
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp
FROM t
)
SELECT user_id, COUNT(*) AS 连续天数
FROM g GROUP BY user_id, grp HAVING COUNT(*) >= 3;
-- 关键技巧:连续日期 - 行号 = 同一个常数,据此分组
例题 3:次日留存率
SELECT COUNT(DISTINCT b.user_id) / COUNT(DISTINCT a.user_id) AS 次日留存率
FROM login_log a
LEFT JOIN login_log b
ON a.user_id = b.user_id
AND DATEDIFF(b.login_date, a.login_date) = 1
WHERE a.login_date = '2024-01-01';
十五、附录
15.1 学习路径
| 阶段 |
目标 |
建议时长 |
练习重点 |
| 第 1 阶段 ⭐ |
会建表、会查单表 |
2~3 天 |
数据类型、DDL、SELECT / WHERE / ORDER BY / LIMIT |
| 第 2 阶段 ⭐⭐ |
会增删改、会统计 |
3~5 天 |
INSERT/UPDATE/DELETE、GROUP BY、JOIN |
| 第 3 阶段 ⭐⭐⭐ |
复杂查询 |
1~2 周 |
子查询、CASE WHEN、各类函数、CTE |
| 第 4 阶段 ⭐⭐⭐⭐ |
数据分析 |
1~2 周 |
窗口函数、透视表、留存/漏斗分析 |
| 第 5 阶段 ⭐⭐⭐⭐⭐ |
性能与原理 |
3~4 周 |
索引、EXPLAIN、事务、锁、MVCC、分库分表 |
练习资源:
练习方法:
- 每题先自己写,写完再对题解;
- 一题多解(窗口函数 / 子查询 / JOIN 各写一遍);
- 写完
EXPLAIN 看有没有走索引;
- 把常用片段存成自己的 snippet 库。
15.2 可直接执行的练习脚本
-- 建库
CREATE DATABASE IF NOT EXISTS sql_study
DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE sql_study;
-- 部门表
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50) NOT NULL,
location VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 员工表
CREATE TABLE employees (
emp_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '员工ID',
name VARCHAR(50) NOT NULL COMMENT '姓名',
dept_id INT COMMENT '部门ID',
salary DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '月薪',
hire_date DATE NOT NULL COMMENT '入职日期',
manager_id BIGINT UNSIGNED COMMENT '上级ID',
PRIMARY KEY (emp_id),
KEY idx_dept (dept_id),
KEY idx_salary (salary),
CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';
-- 订单表
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
order_date DATE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT '待付款',
KEY idx_user_date (user_id, order_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入数据
INSERT INTO departments VALUES
(10,'技术部','北京'), (20,'市场部','上海'), (30,'财务部','北京');
INSERT INTO employees (name, dept_id, salary, hire_date, manager_id) VALUES
('张三', 10, 8000.00, '2021-03-01', 3),
('李四', 10, 12000.00, '2020-07-15', 3),
('王五', 20, 15000.00, '2019-01-20', NULL),
('赵六', 30, 9000.00, '2022-11-05', 3),
('孙七', NULL, 7000.00, '2023-02-10', NULL);
INSERT INTO orders (user_id, amount, order_date, status) VALUES
(1, 120.50, '2024-01-05', '已付款'),
(1, 300.00, '2024-02-11', '已付款'),
(2, 80.00, '2024-01-20', '已退款'),
(3, 500.00, '2024-03-01', '已付款');
-- 验证
SELECT * FROM employees;
SELECT * FROM departments;
SELECT * FROM orders;
15.3 术语速查表
| 缩写 / 术语 |
全称 / 含义 |
| DDL / DML / DQL / DCL / TCL |
语句五大分类,见 §1.1 |
| ACID |
Atomicity Consistency Isolation Durability,事务四特性 |
| MVCC |
Multi-Version Concurrency Control,多版本并发控制 |
| WAL |
Write-Ahead Logging,预写日志 |
| B+Tree |
索引默认数据结构 |
| ICP |
Index Condition Pushdown,索引下推 |
| MRR |
Multi-Range Read,多范围读 |
| OLTP / OLAP |
在线事务处理(业务系统)/ 在线分析处理(报表数仓) |
| QPS / TPS |
每秒查询数 / 每秒事务数 |
| 回表 |
二级索引查到主键后再查聚簇索引 |
| 覆盖索引 |
Covering Index,查询列全在索引中,无需回表 |
| 最左前缀 |
联合索引的匹配规则 |
| 深分页 |
OFFSET 很大的分页查询 |
| 页分裂 |
乱序插入导致 B+Tree 节点分裂,性能下降 |
📌 本文基于 MySQL 8.0。要点回顾三句话:
- 写查询:想清楚「要什么列、从哪张表、关联谁、怎么过滤、怎么分组、怎么排序」;
- 建表:类型选最小、金额用 DECIMAL、时间用 DATETIME、字符集 utf8mb4、字段尽量 NOT NULL;
- 调优 :先
EXPLAIN,看 type / key / rows / Extra,消灭 ALL、filesort、temporary。