简介:一份覆盖 MySQL 从安装启动到高级应用的成体系学习笔记,面向希望系统掌握数据库操作、备考或从事后端开发的数据从业者。内容以 1000 行精心整理的命令与要点为主线,从 Windows 服务启动、连接与断开服务器、库表增删改查,到存储引擎选型、字符集与排序规则、临时表、视图、触发器、存储过程、事务与索引优化等高级特性均有涉及,适合按章节逐段对照练习,也便于面试前快速回顾。资源为单个 docx 文档,约 49KB,体积小巧却信息密集,便于阅读、检索与二次编辑,可快速定位日常开发或学习所需知识点。目前已有 477 人学习,适合初学者打基础,也适合有经验者查漏补缺。笔记对每条命令都配有简明注释,例如建表时的字段属性、表选项、修改表结构、重命名等均有实例说明,读者可据此搭建自己的 MySQL 速查手册。
1. 为什么还缺一份 1000 行的 MySQL 学习笔记
学 MySQL 最大的困境从来不是资料少,而是资料太多。官方手册按章节铺开,培训课程动辄几十个小时,等到真要写一条 UPDATE 时,能记住的往往只剩 SELECT *。1000 行学习笔记这个体量,恰好卡在“手册”和“小抄”之间:它装不下内核源码级别的实现,却能覆盖日常开发九成会用到的建库、CRUD、索引、事务与备份操作,这也正是“史上最全珍藏版”这类标题真正有资格承载的内容密度。
笔记的价值不在堆页数,而在每一行都能对上具体场景。连接器、优化器、存储引擎这三层架构如何影响你写 SQL;组合索引为什么有时完全失效;InnoDB 默认的 REPEATABLE READ 下为什么还能读到旧版本数据——这些才是值得反复看的部分,而不是把 CREATE TABLE 抄十遍。
这份笔记按我自己的整理顺序展开:先高频 SQL 与执行顺序,再索引和执行计划,然后事务与 MVCC,最后落到安装排查和备份这些终端操作。适合已经写过 SELECT、但没把 MySQL 系统串成一条线的后端工程师,也适合面试前想快速把知识点连成链的人。每条规则后面都给了可复现的命令和参数,可以直接抄走改着用。
2. 从建库到查数:MySQL 高频 SQL 怎么记才不白背
2.1 建库建表的第一行决定后面三年:utf8mb4 与 InnoDB
笔记里如果把建表当填空题,只记字段不记理由,三个月后翻出来照样不敢改。字符集和存储引擎这两项,我一般会写清楚选择依据,后面所有 SQL 行为都建立在它们之上。
CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE shop; CREATE TABLE t_user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', user_name VARCHAR(64) NOT NULL COMMENT '登录名', phone CHAR(11) DEFAULT NULL COMMENT '手机号', age TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '年龄', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;说明:utf8mb4 是必选项而不是可选项,MySQL 8.0 起默认就是这个,老库迁移时要确认所有表的 charset 已改,否则 emoji 和生僻字直接报 Incorrect string value。CHAR(11) 存手机号这类定长数据,避免了 VARCHAR 的变长头和页内碎片。updated_at 依赖 ON UPDATE 自动刷新,程序里少写一行,也能保证任何途径的 UPDATE 都留下时间戳。
字段类型上有个容易翻车的细节:TINYINT 的范围只有 -128 到 127,unsigned 上限 255。笔记里若出现 age + 1 这类自增更新,先确认列宽,线上出现过一次 TINYINT 溢出报错把整个事务回滚的案例。id 用 BIGINT UNSIGNED 也是同样的考虑,INT 自增在千万行规模下真的会顶到天花板。
注意:不要为了省那几 MB 把状态字段设成 ENUM,后续加枚举值要 ALTER TABLE,业务上线的速度会被一次 DDL 卡住。TINYINT + 注释表是运维更认可的写法。
2.2 UPDATE 语法与子查询:ERROR 1093 和“忘写 WHERE”两个坑
高频 SQL 里风险最高的不是 SELECT 而是 UPDATE。笔记中值得单独做一节的,一是同表子查询更新报错,二是 WHERE 条件缺失,三是整数溢出。这三个问题我都踩过。
-- 反例:MySQL 不允许 UPDATE 的目标表出现在子查询的 FROM 里 UPDATE t_user SET age = age + 1 WHERE id IN (SELECT id FROM t_user WHERE status = 0); -- ERROR 1093: You can't specify target table 't_user' for update in FROM clause -- 解法一:把子查询包成派生表,让 MySQL 先物化 UPDATE t_user SET age = age + 1 WHERE id IN ( SELECT id FROM (SELECT id FROM t_user WHERE status = 0) tmp ); -- 解法二:JOIN 更新,语义更直观 UPDATE t_user u JOIN (SELECT id FROM t_user WHERE status = 0) tmp ON u.id = tmp.id SET u.age = u.age + 1;说明:ERROR 1093 的根源是 MySQL 无法确定“读了同一张表”是否安全。包一层派生表之后,子查询结果被物化成临时表,更新目标就和数据源区分开了。JOIN 更新表达的是同一件事,关联键用主键时性能没有问题。多表 UPDATE 时,SET 里写别名列(u.age),WHERE 里的条件也建议全部带表别名,避免字段歧义。
update 语法里还有两个边界:第一,UPDATE 支持 LIMIT 和 ORDER BY,低风险的批处理可以写成 UPDATE t_user SET status = 0 WHERE … LIMIT 200,配合循环逐批提交,避免一次锁太多行;第二,想给某列设置默认值 0,别在 UPDATE 里写死,用 ALTER TABLE t_user ALTER COLUMN age SET DEFAULT 0 让模型层去兜底。生产环境的常见做法,是先 SELECT COUNT(*) 确认影响行数,再执行 UPDATE,这是 1000 行笔记里最便宜也最值钱的一条。
2.3 SELECT 执行顺序与 JOIN 语义:排序、去重和 ON/WHERE 的边界
SELECT 写得流畅的前提,是脑子里有一张执行顺序表。顺序决定了很多“为什么这里报错”:
| 顺序 | 关键字 | 作用 |
|---|---|---|
| 1 | FROM / JOIN | 确定数据源并生成中间结果 |
| 2 | ON | 对 JOIN 结果做匹配过滤 |
| 3 | WHERE | 行级过滤 |
| 4 | GROUP BY | 按列分组 |
| 5 | HAVING | 分组后的过滤 |
| 6 | SELECT | 投影列与表达式 |
| 7 | DISTINCT | 去掉重复行 |
| 8 | ORDER BY | 排序 |
| 9 | LIMIT | 限定行数 |
由这张表能推出三条直接可用的规则:WHERE 里不能用 SELECT 别名,因为 WHERE 执行在第 6 步之前;ORDER BY 反而可以用别名,它执行在 SELECT 之后;HAVING 在没有 GROUP BY 时配合聚合函数才有意义,严格模式下散列在 SELECT 里会直接报错。
排序和去重是被问得最多的两个点。ORDER BY 走索引时是顺序读,否则就是 Using filesort,代价是数据先装进排序缓冲区;所以 order by 的列尽量放进组合索引,且排序方向保持一致。至于“OR 能去重吗”这类问题,答案是去重只认 DISTINCT 和 GROUP BY;OR 在 JOIN 条件里出现时,连接条件变成一个不等式集,既难走索引,又容易和 ON 另一侧的匹配组合出重复行。常见做法是拆成两条查询用 UNION 合并,让每条分支都吃到自己的索引。
JOIN 的语义区分是这样的:
-- 查所有用户及其 status=1 的订单,无订单的用户也保留 SELECT u.id, o.order_no FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id AND o.status = 1; -- 只查有有效订单的用户,WHERE 把 NULL 行滤掉了 SELECT u.id, o.order_no FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id WHERE o.status = 1;说明:LEFT JOIN 时,第二个查询的结果等价于 INNER JOIN,因为 WHERE 在 JOIN 完成后执行,把未匹配产生的 NULL 行过滤掉了。第一条语句才是真正意义上的“左表全保留,右表按需匹配”。这就是 JOIN 含义里最核心、也最容易在面试里一问就倒的边界。深分页的 LIMIT 100000, 20 这种写法,越翻越慢,常见优化是记录上一页最大 id,用 WHERE id > ? ORDER BY id LIMIT 20 代替偏移量。
3. MySQL 索引与执行计划:笔记里真正值钱的 200 行
3.1 索引为什么快:B+ 树、聚簇索引与回表
索引笔记不需要背 B+ 树的插入分裂细节,但要记结论:B+ 树非叶节点只存键和指针,一层能装上千个键,三到四层就能覆盖千万行,查询次数等于树高,和行数几乎无关。这就是索引快的基本盘。
InnoDB 的聚簇索引把主键直接当作索引文件,叶子节点里就是整行数据;二级索引的叶子节点存的是主键值。所以用二级索引查一条 SELECT *,要先扫二级索引拿到主键,再回聚簇索引取整行,这个动作叫回表。回表是随机读,行数一多成本就上来。反直觉的结论是:当查询要读取的行数超过全表的 15% 到 20%(经验值),优化器常常放弃索引改走全表扫描,因为顺序读比大量随机读更便宜。理解这一点,才能理解 mysql 执行计划里出现的 ALL 不一定都是坏事。
3.2 创建索引的语法与最左前缀原则:组合索引怎么设计
-- 在已有表上加索引 ALTER TABLE t_user ADD INDEX idx_user_name (user_name); -- 组合索引,条件里最常用的是 age + status 这个组合 CREATE INDEX idx_age_status ON t_user (age, status); -- 删除索引,DDL 会锁表,低峰期执行 DROP INDEX idx_age_status ON t_user; -- 查看表上有哪些索引 SHOW INDEX FROM t_user;说明:ADD INDEX 和 CREATE INDEX 作用相同,前者适合建表后补,后者适合脚本里语义化创建。组合索引的列顺序是设计重点,字段顺序决定了它能被哪些查询复用:
| 查询条件 | 是否走 idx_age_status | 说明 |
|---|---|---|
| WHERE age = 20 | 走 | 命中最左列 |
| WHERE age = 20 AND status = 1 | 走 | 完全命中 |
| WHERE status = 1 | 不走 | 跳过最左列 |
| WHERE age > 20 | 走 | range 扫描 |
| WHERE status = 1 AND age = 20 | 走 | 优化器会做等值重排 |
最左前缀原则是三句话:条件里的列必须从索引最左列开始连续匹配;范围列(>、<、between)后面的列不再走索引;等值列放前面,范围列放后面。设计组合索引的常见做法,是把区分度高的列放在前面,并优先覆盖“等值 + 排序”的组合,比如 (age, created_at) 既过滤又免掉 filesort。
还有几个索引失效的高频场景,笔记里必须记:索引列上做函数运算(WHERE DATE(created_at) = '2025-06-01')不走索引,应改写成 created_at >= '2025-06-01' AND created_at < '2025-06-02' 的范围条件;隐式类型转换(phone 是 CHAR,却拿数字去比较)会让索引失效;LIKE '%abc' 前置通配符用不上索引,'abc%' 可以;OR 条件要两边的列都有索引且优化器用 index merge 才可能走,最稳妥是拆 UNION。
3.3 用 EXPLAIN 读执行计划:type、key、rows 三个信号
索引建得对不对,别靠猜,用 EXPLAIN。这一节是 mysql 性能调优里投入产出比最高的部分。
EXPLAIN SELECT user_name, age, status FROM t_user WHERE age > 20 AND status = 1 ORDER BY created_at;执行计划输出是一个表,我一般只先看四列:
| 列 | 含义 | 重点关注 |
|---|---|---|
| type | 访问类型 | 至少要 range 及以上 |
| key | 实际用到的索引 | 是否为预期索引 |
| rows | 估算扫描行数 | 与真实行数对比 |
| Extra | 附加信息 | 出现 filesort、temporary 要警惕 |
type 从好到坏排列:system/const(主键或唯一键等值)、eq_ref(被驱动表按主键连接)、ref(普通索引等值)、range(索引范围扫描)、index(扫全索引树)、ALL(全表扫描)。线上慢 SQL 的排查路径是固定的:先看 type 是否掉到 index 或 ALL,再看 key 是不是设计中的那个索引,最后看 Extra 里有没有 Using filesort 和 Using temporary——这两个出现任何一个,都说明排序或分组没有吃到索引。
rows 是估算值,不是实际值,但它能暴露统计信息是否过期。如果 EXPLAIN 估算一万行,实际查出来只有几十行,多半是表统计信息没更新,执行 ANALYZE TABLE t_user 后再看。补充一点:连接池只能省下建连和握手开销,SQL 本身慢,开多少连接池都救不回来;慢 SQL 的定位要靠慢查询日志(long_query_time 设为 1 秒)加 EXPLAIN 逐条过,这才是 mysql 性能调优的日常节奏。
提示:EXPLAIN 不会真正执行 SQL,它只生成优化器认为可行的执行路径。改完索引之后记得用 ANALYZE TABLE 刷新统计信息,否则 rows 列会一直参考旧数据。
4. MySQL 事务隔离与 MVCC:锁与快照的博弈
4.1 四种隔离级别分别在防什么:脏读、不可重复读与幻读
事务笔记的第一张表一定是隔离级别。MySQL 的可重复读(RR)是默认值,但很多人说不出它和读已提交(RC)到底差在哪一列参数上:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不会 | 可能 | 可能 |
| REPEATABLE READ(默认) | 不会 | 不会 | 快照读下基本不会 |
| SERIALIZABLE | 不会 | 不会 | 不会 |
脏读是读到未提交的数据;不可重复读是同一个事务里两次 SELECT 读到不同行;幻读是同一个事务里两次范围查询,第二次多出或少了行。MySQL 的 RR 靠 MVCC 和 next-key 锁把幻读压到了极小范围,所以默认隔离级别可以直接用。
-- 查看当前隔离级别 SELECT @@transaction_isolation; -- 会话级临时切换,只影响当前连接 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 全局级切换,8.0 里对已有连接不生效 SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;说明:互联网公司线上很多用 RC,原因是 RC 下间隙锁大幅减少,并发度更高,binlog 配合 row 格式就够了;RR 默认用于对一致性要求高、允许锁开销的业务。SET GLOBAL 只对新连接生效,DBA 通常会把参数写进 my.cnf 再滚动重启,笔记里要区分 session 和 global 两个作用域。
4.2 MVCC 的隐藏列与 Read View:为什么 RR 能重复读
MVCC 是 MySQL 实现读写不互斥的核心理念。InnoDB 在每行数据后面加了两列:DB_TRX_ID 记录最后修改它的事务 id,DB_ROLL_PTR 指向上一个版本的 undo log 位置。行数据因此形成一条版本链。
读数据时,事务会生成一个 Read View,里面记录了当前活跃事务列表和最小/最大事务 id。判断规则可以简化成一句:数据行的 trx_id 小于 Read View 中最早活跃事务 id,或不在活跃列表内且小于最大 id,就可见;否则沿版本链往回找。RC 和 RR 的本质差别只有一个:RC 每条语句生成新的 Read View,RR 在事务第一次读时生成并复用。
-- 事务 A START TRANSACTION; SELECT * FROM t_user WHERE id = 1; -- 第一次读,生成 Read View -- 此时事务 B 更新 id=1 并提交 SELECT * FROM t_user WHERE id = 1; -- 仍读到第一次的值 COMMIT;说明:在 RR 下,事务 A 的两次 SELECT 结果完全一致,这就是“可重复读”的机制根源;把隔离级别换成 RC,第二次 SELECT 会读到事务 B 提交后的新值。UPDATE、DELETE、SELECT ... FOR UPDATE 这类当前读不走版本链,它们读最新版本并对行加锁,这也是为什么“明明开了事务还读不到别人未提交的数据”这类问题,本质上要先分清快照读和当前读。
4.3 行锁、间隙锁与死锁排查:当前读和锁范围
加了 for update 的当前读,锁的范围取决于条件和索引:
-- 会话 A 开事务,锁住主键 id=8 这一行 START TRANSACTION; SELECT * FROM t_user WHERE id = 8 FOR UPDATE; -- 会话 B 对同一行 UPDATE 会阻塞,直到 A 提交 UPDATE t_user SET status = 0 WHERE id = 8; -- 查当前事务和锁等待 SELECT * FROM information_schema.innodb_trx\G SELECT * FROM information_schema.innodb_lock_waits\G -- 死锁发生后的第一现场 SHOW ENGINE INNODB STATUS\G说明:主键等值命中时是记录锁,只锁一行;范围条件(age BETWEEN 10 AND 20)在 RR 下会加间隙锁或 next-key 锁,锁住索引区间,防止其他事务插入,这是 RR 抑制幻读的重要手段。最容易引发线上事故的写法,是 UPDATE 或 DELETE 的 WHERE 列没有索引——InnoDB 找不到精确的索引区间,只能给大量记录加锁,RR 下还会升级成大面积间隙锁,表现为“一句话把整张表堵死”。
死锁的常规应对是防而不是救:多个事务按相同顺序访问资源;事务体尽量短,把无关查询挪到事务外;每个 SQL 都尽量走索引,缩小锁范围。真正的死锁发生概率低,InnoDB 会自动回滚代价较小的事务,接口侧报错一般是 1213,接到这个错误码做一次重试即可,笔记里应该把“死锁不一定都是 bug”这个观念写进去。
5. 把 MySQL 笔记落到终端:安装配置、general_log 与备份命令
笔记最后留一节终端命令,是因为装上 MySQL 却连不上、找不到初始密码、不知道程序发的哪条 SQL 慢,前面所有知识点都用不上。这一节我把最常被检索的几类问题压缩成了可以直接执行的命令。
5.1 安装配置后的第一件事:初始密码与端口号
# RPM 包方式安装的 MySQL 8.0,初始随机密码在日志里 sudo grep 'temporary password' /var/log/mysqld.log # 确认服务与端口 3306 的监听状态 sudo systemctl status mysqld ss -lntp | grep 3306 # 登录连通性测试 mysqladmin -uroot -p ping说明:从官网下载安装包时注意选 MySQL Community Server 8.0,而不是最新的 innovation 版本,后者迭代快,生产环境兼容性需要更多验证。Docker 方式安装可以省去初始化步骤:docker run --name mysql8 -e MYSQL_ROOT_PASSWORD=你的密码 -p 3306:3306 -d mysql:8.0,但要把数据目录用 -v 挂到宿主机,容器删掉数据还在。用 Workbench 或 Navicat 连接时,主机名填 127.0.0.1、端口 3306、用户名 root,密码用安装时设置的,连不上先 ping 3306 而不是怀疑密码。
5.2 打开 general_log,看清每一条真实 SQL
连接池掩盖了大量行为,程序实际发出去的 SQL 和心里想写的经常不一样。把 general.log 打开一次,一切现形:
SET GLOBAL general_log = 'ON'; SET GLOBAL general_log_file = '/var/log/mysql/general.log'; -- 定位完问题立刻关闭,生产环境不建议开启 SET GLOBAL general_log = 'OFF';说明:general_log 记录所有到达服务器的 SQL,包括成功的和失败的,排查“为什么某条语句耗时高但慢日志里没有”很有效。打开后另开一个终端执行 tail -f /var/log/mysql/general.log 就能实时观察。存储过程这类写法,笔记里只需要抄两处:声明时用 DELIMITER $$ 临时改结束符,过程体内保持 CREATE PROCEDURE 开头、BEGIN...END 收尾即可。
5.3 备份与恢复:一条 mysqldump 和三个必带参数
# 单事务一致备份,InnoDB 表不锁业务写 mysqldump -uroot -p --single-transaction \ --default-character-set=utf8mb4 shop > shop_$(date +%F).sql # 恢复 mysql -uroot -p shop < shop_2025-06-01.sql说明:--single-transaction 用一致性快照实现备份,不加它会退化成 LOCK TABLES,备份期间业务写入全部阻塞;--default-character-set=utf8mb4 保证 emoji 和中文注释不变成乱码;备份文件名带日期是防止覆盖上一次备份。恢复完的测试库不要急着删,把第 3 章的 EXPLAIN 原样跑一遍,key 列和 Extra 列会直接告诉你索引笔记里哪几行值得再背一轮。
本文还有配套的精品资源,点击获取