导读
你是否遇到过这种场景:一条 SQL 查出来要好几秒,数据量再大一点直接超时?90% 的性能问题都出在索引没用对。
这篇文章从零开始——先装好 MySQL,然后一步步讲清楚怎么建表、怎么加主键和外键、B+ 树为什么快、怎么读懂 EXPLAIN 的执行计划、怎么用慢查询日志找到拖后腿的 SQL,最后讲到事务和隔离级别。每个命令都带注释,复制粘贴就能跑。
预计阅读时间 15 分钟,配合同步操作约 30 分钟即可掌握全流程。文末附 Linux/macOS 和 Windows(WSL)双平台一键脚本,开箱即用。
一、安装 MySQL(Linux / macOS / WSL)
1.1 Debian / Ubuntu 系
# 更新软件包列表(apt 必须先做这一步,否则会报"找不到软件包")
sudo apt update
# 安装 MySQL 服务器(默认会装最新版,当前最新稳定版为 8.0.x)
sudo apt install -y mysql-server
# 启动并设置开机自启
sudo systemctl start mysql
sudo systemctl enable mysql
# 检查运行状态
sudo systemctl status mysql
国内镜像提示:如果
apt update很慢,可以替换为清华源。编辑/etc/apt/sources.list,把deb http://archive.ubuntu.com/ubuntu改成deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu。修改后重新sudo apt update。
1.2 CentOS / RHEL 系
# CentOS 8+/RHEL 8+ 使用 dnf(yum 已逐渐被替代)
sudo dnf install -y mysql-server
# 启动
sudo systemctl start mysqld
sudo systemctl enable mysqld
sudo systemctl status mysqld
1.3 macOS
# 用 Homebrew 安装(没有 Homebrew 的先跑:/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)")
brew install mysql
brew services start mysql
GitHub 直连慢的同学可以用
ghfast.top代理加速下载 Homebrew 安装脚本,格式为在原始 URL 前加https://ghfast.top/前缀。
1.4 初始化安全设置
安装完成后运行安全脚本:
# 交互式安全向导:设置 root 密码、删除匿名账号、禁止 root 远程登录等
sudo mysql_secure_installation
注意:这个步骤必须做,否则你的数据库没有任何安全防护。脚本会问你:设置 root 密码、移除匿名用户、禁止 root 远程登录、删除 test 库、刷新权限——全部选 Y 即可。如果你用的是 MariaDB(MySQL 的社区分支),同样适用。
1.5 进入 MySQL
# 以 root 用户登录(首次无密码直接回车,之后需要输入上面设置的密码)
mysql -u root -p
成功后看到 mysql> 提示符,说明安装完成。输入 exit 退出。
公开源码提醒:全文末尾附有双系统一键脚本完整源码,不放心任何命令都可以先看完再跑。也欢迎复制给任何 AI 审查。
二、建表与约束
2.1 创建数据库
-- 创建一个名为 shop 的数据库(UTF-8 编码,支持中文)
CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 切换到 shop 数据库
USE shop;
白话解释:
utf8mb4是 MySQL 真正的 UTF-8(普通的utf8只支持最多 3 字节,存不了 emoji)。COLLATE utf8mb4_unicode_ci表示排序时不分大小写(ci= case-insensitive)。
2.2 创建数据表
我们以一个电商基础结构为例,创建三张表:
-- ==============================
-- 表1:用户表(users)
-- ==============================
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键自增 ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,不能重复也不能为空',
email VARCHAR(100) NOT NULL COMMENT '邮箱',
phone VARCHAR(20) COMMENT '手机号(允许为空,有些用户不设)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,自动生成'
) ENGINE=InnoDB COMMENT='用户表';
-- 插入几条测试数据
INSERT INTO users (username, email) VALUES
('alice', '[email protected]'),
('bob', '[email protected]'),
('charlie', '[email protected]');
术语解释:
- PRIMARY KEY(主键):每行记录的唯一标识,不能为空且不能重复。一张表只能有一个主键。就像每个人的身份证号码。
- AUTO_INCREMENT:每次插入新行时自动递增,不需要手动指定 id 值。
- NOT NULL:这个字段必须有值,不允许存 NULL(空值)。
- UNIQUE:这个字段的值在所有行中必须唯一。就像学号不能重复。
- ENGINE=InnoDB:指定存储引擎。InnoDB 是 MySQL 5.5 之后的默认引擎,支持事务和外键——如果你的项目用到事务或外键约束,必须用 InnoDB(不要选 MyISAM)。
- TIMESTAMP DEFAULT CURRENT_TIMESTAMP:默认值为当前时间戳,插入时不用手动填。
-- ==============================
-- 表2:商品表(products)
-- ==============================
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键自增 ID',
name VARCHAR(200) NOT NULL COMMENT '商品名称',
price DECIMAL(10, 2) NOT NULL COMMENT '价格,精确到分(10位总长度,小数占2位)',
stock INT DEFAULT 0 COMMENT '库存量,默认为 0',
category VARCHAR(50) COMMENT '分类',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB COMMENT='商品表';
INSERT INTO products (name, price, stock, category) VALUES
('机械键盘', 399.00, 50, '数码'),
('无线鼠标', 89.90, 200, '数码'),
('Python 入门教程', 49.00, 1000, '图书'),
('咖啡杯', 35.00, 300, '生活');
DECIMAL(10,2):为什么要用 DECIMAL 而不是 FLOAT/DOUBLE 存价格?因为浮点数有精度损失——
0.1 + 0.2 != 0.3是所有编程语言都有坑。DECIMAL 以字符串形式精确存储,适合金额。
-- ==============================
-- 表3:订单表(orders)
-- ==============================
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '订单 ID',
user_id INT NOT NULL COMMENT '关联用户表的 id(外键)',
product_id INT NOT NULL COMMENT '关联商品表的 id(外键)',
quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量',
order_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE COMMENT '外键约束:用户删除时级联删其订单',
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT COMMENT '外键约束:有订单引用此商品时不允许删除'
) ENGINE=InnoDB COMMENT='订单表';
INSERT INTO orders (user_id, product_id, quantity) VALUES
(1, 1, 1), -- alice 买了机械键盘 x1
(1, 3, 2), -- alice 买了 Python 教程 x2
(2, 2, 3); -- bob 买了无线鼠标 x3
术语解释:
- FOREIGN KEY(外键):建立两张表之间的关联关系。比如
orders.user_id REFERENCES users(id)表示 orders 表里的 user_id 必须能在 users 表里找到对应的 id。就像你借书时必须登记"向谁借的"。- ON DELETE CASCADE:当被引用的行被删除时,自动删除引用它的行。比如删掉 alice 这个用户,她名下所有订单也一起清空。
- ON DELETE RESTRICT:阻止删除!如果有订单引用了这个商品,你就不能删除它——防止误删导致历史订单变成"幽灵商品"。
验证一下关联查询是否正常工作:
-- 联合查询:查看 alice 的所有订单及商品信息
SELECT u.username, p.name, o.quantity, o.order_time
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id
WHERE u.username = 'alice';
预期输出:
+----------+------------------+----------+---------------------+
| username | name | quantity | order_time |
+----------+------------------+----------+---------------------+
| alice | 机械键盘 | 1 | 2026-08-26 10:00:00 |
| alice | Python 入门教程 | 2 | 2026-08-26 10:00:00 |
+----------+------------------+----------+---------------------+
三、索引白话原理解析
3.1 什么是索引?为什么需要?
白话版类比:想象一本 500 页的书。你要找"MySQL"这个词出现在哪几页——
- 没有索引:从第 1 页翻到第 500 页,一页一页看(这叫全表扫描,全表扫描就是英文说的 Full Table Scan,简称 FTS。数据量大时极慢)。
- 有索引:直接翻到书末的"索引"部分,查到"MySQL → 见第 123、345、378 页",跳过去就读到了。
在数据库中,没有索引时 MySQL 逐行扫描每一行数据;有索引时通过数据结构(通常是 B+ 树)可以直接定位到目标位置。
3.2 B+ 树是什么?
这是面试官最常问的问题,也是实际工作中最重要的概念。
B+ 树的核心特征:
- 所有数据都存在叶节点(Leaf Node)。非叶节点(Interior Node / 根节点)只存键值和指向子节点的指针——相当于目录只有章节名和页码,正文内容全在书中。
- 叶节点之间用链表连接。这使得范围查询(
WHERE age BETWEEN 20 AND 30)不需要回退到根节点重新搜索,沿着叶链表一路扫过去就行。 - 非叶节点只存键,不存完整数据行。这意味着单个磁盘页能放下更多键值,树的高度更矮。
- 多路平衡。普通二叉树可能退化成链表(最坏情况查 N 行要 N 次 IO),B+ 树的每个节点可以有几百上千个子节点,千万级数据只需要 3~4 层。
为什么比哈希表好? 哈希表查单条记录是 O(1),但它不支持范围查询、不支持排序、不支持 LIKE 模糊查询。B+ 树虽然最差是 O(log n),但功能全面,而且范围查询特别高效。
配图说明:本文章首部的
01.svg画出了完整的 B+ 树结构——红色根节点决定第一层走哪条路,蓝色中间节点继续缩小范围,绿色叶节点存储真实数据。查找 key=55 只需 3 步磁盘 IO。
3.3 聚簇索引 vs 二级索引
这是 MySQL InnoDB 特有的重要概念:
| 类型 | 描述 | 例子 |
|---|---|---|
| 聚簇索引(Clustered Index) | 数据行本身就按主键排序存储在叶节点中。InnoDB 的聚簇索引=主键索引。一张表只能有一个聚簇索引。 | 上面的 orders.id(主键就是聚簇索引) |
| 二级索引(Secondary Index) | 额外建的索引,叶节点存的是主键值,不是完整数据行。查二级索引时需要"回表"——先拿到主键,再去聚簇索引里找完整行。 | 在 users.email 上建索引后,查到邮箱对应的主键 id,再拿着 id 去聚簇索引取该行全部数据 |
白话:聚簇索引就像图书馆——书本身就是按编号排列放在书架上的。二级索引就像检索系统——查到一本书的编号后,你还得去书架上把那本书取下来。
3.4 创建和使用索引
-- ===== 方法1:建表时直接定义索引 =====
CREATE INDEX idx_username ON users(username);
-- 在 users 表的 username 列上创建了普通索引
-- ===== 方法2:ALTER TABLE 为已有表加索引 =====
ALTER TABLE products ADD INDEX idx_category (category);
-- 为已有的 products 表加了一个分类索引
-- ===== 方法3:唯一索引(比普通索引多一层"不能重复"的约束)=====
CREATE UNIQUE INDEX idx_email ON users(email);
-- 保证 email 列不会有两行相同的值
-- 也可以在建表时用 CONSTRAINT 语法:ALTER TABLE users ADD UNIQUE(unixx_IX_index_name email);
-- ALTER TABLE tbl_name ADD UNIQUE index_name (column_list);
ALTER TABLE users ADD UNIQUE INDEX idx_email_unique (email);
-- ===== 查看某张表有哪些索引 =====
SHOW INDEX FROM users;
-- 输出会显示索引名、列名、是否唯一、索引类型等信息
-- ===== 联合索引(多列一起建索引)=====
ALTER TABLE orders ADD INDEX idx_user_time (user_id, order_time);
-- 先按 user_id 排,相同 user_id 再按 order_time 排
-- 这叫做最左前缀原则:只 WHERE user_id 能用索引,只 WHERE order_time 则不能用(因为没走在最左边)
什么时候该加索引?
- WHERE 条件里的列经常被查
- JOIN 关联的列
- ORDER BY 排序的列
- 列的值比较分散(区分度高,比如用户 ID;如果只有两个值 like gender,建索引意义不大)
什么时候不该加?
- 很少被查的列
- 频繁 UPDATE 的列(每次更新都要同步维护索引树)
- 区分度很低的列(比如性别——只有男女两种值,MySQL 优化器很可能放弃索引改走全表扫描)
-- 删除不再需要的索引
DROP INDEX idx_category ON products;
-- 或者
ALTER TABLE products DROP INDEX idx_category;
-- 删除主键(先取消 AUTO_INCREMENT 再删)
ALTER TABLE users MODIFY id INT NOT NULL;
ALTER TABLE users DROP PRIMARY KEY;
四、EXPLAIN 怎么看执行计划
4.1 什么是执行计划?
当你执行一条 SELECT 语句之前,MySQL 的优化器会先分析一下:“我该怎么查最快?“然后生成一份执行计划。你可以通过 EXPLAIN 关键字看到这个计划——不实际执行查询,只告诉你"打算怎么查”。
-- 用法:在 SELECT 前面加上 EXPLAIN 即可
EXPLAIN SELECT * FROM users WHERE username = 'alice';
4.2 核心字段解读
以下是一个典型的 EXPLAIN 输出及每个字段的含义:
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
| 1 | SIMPLE | users | NULL | const| PRIMARY |PRIM | 202 |const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
重点看这几个字段:
| 字段 | 含义 | 理想值 |
|---|---|---|
| type | 访问类型(越往下越快) | const > eq_ref > ref > range > index > ALL |
| key | 实际使用的索引名 | 不为 NULL |
| rows | 估计要扫描的行数 | 越小越好 |
| Extra | 额外信息 | 最好没有,出现 USING FILESORT / USING TEMPORARY 要警惕 |
4.3 type 字段详解(从快到慢)
这是最重要的一列,决定了查询的效率等级:
| type | 等级 | 含义 | 举例 |
|---|---|---|---|
| system | ★★★★★ | 表只有一行(系统表) | 极少遇到 |
| const | ★★★★★ | 通过主键/唯一索引查到一行 | WHERE id = 1 |
| eq_ref | ★★★★☆ | 唯一索引扫描,每一行匹配到另一表的一行 | JOIN 时用主键关联 |
| ref | ★★★☆☆ | 非唯一索引扫描,一行匹配多行 | WHERE username = 'alice' |
| range | ★★☆☆☆ | 索引范围扫描 | WHERE age > 20、WHERE id IN (1,3,5) |
| index | ★☆☆☆☆ | 全索引树扫描(比全表好一点) | SELECT count(*) FROM users(覆盖索引) |
| ALL | ☆☆☆☆☆ | 全表扫描(最差) | WHERE email LIKE '%@gmail.com'(前缀通配无法走索引) |
实战判断标准:只要 type 不是 ALL,说明用到了索引。range 以上算良好,ref 以上算优秀。
4.4 对比实验:加索引前后的变化
让我们用一个真实的例子来看效果。先往 orders 表插一些数据模拟大数据量:
-- 批量插入 10000 条测试数据(在 MySQL 命令行里这样跑)
DELIMITER //
CREATE PROCEDURE insert_test_data()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 10000 DO
INSERT INTO orders (user_id, product_id, quantity)
VALUES ((i % 3) + 1, ((i % 4) + 1), (i % 10) + 1);
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
CALL insert_test_data();
DROP PROCEDURE insert_test_data;
不加索引的查询(比如在 email 上用 LIKE 前缀通配符):
EXPLAIN SELECT * FROM users WHERE email LIKE '%alice%' \G
注意 \G 是以纵向格式输出(更适合长输出)。此时 type 会是 ALL(全表扫描),rows 会显示需要检查所有行数。
加了索引后的查询:
EXPLAIN SELECT * FROM users WHERE username = 'alice' \G
此时 type 会变成 ref 或 const(取决于是否有唯一索引),rows 通常只会是 1。
实用技巧:想让 SQL 写得不好的人(包括我自己)少踩坑,可以让 AI 帮你提前审一遍——比如拿云间中转站的模型(cloudzone-api.cyou)快速跑一下"这条 SQL 会不会慢”,零门槛,几十块钱就能搞定一个月的用量。
4.5 Extra 中的警告信号
| 值 | 含义 | 如何处理 |
|---|---|---|
| Using filesort | MySQL 需要在内存或磁盘中做一次额外的排序(没用到索引排序) | 检查 ORDER BY 的列是否建立了索引 |
| Using temporary | 使用了临时表来结果集去重或分组(常见于 GROUP BY + DISTINCT) | 考虑加索引或在应用层处理 |
| Using index(覆盖索引) | 好消息! 查询所需的数据全在索引中,不用再回表 | 继续保持,这是最优方案 |
| Using where | 在存储引擎层做了条件过滤(type 为 ALL 时要特别注意) | 给 WHERE 列加索引 |
五、慢查询日志——找出拖后腿的 SQL
5.1 什么是慢查询?
慢查询是指执行时间超过设定阈值的 SQL 语句。MySQL 提供了内置的**慢查询日志(Slow Query Log)**功能,自动记录这些 SQL,方便排查性能瓶颈。
5.2 开启慢查询日志
-- 查看当前慢查询阈值(单位:秒,默认 10 秒)
SHOW VARIABLES LIKE 'long_query_time';
-- 设置为 1 秒(生产环境建议 1~2 秒,开发阶段可设 0.5 秒抓更多数据)
SET GLOBAL long_query_time = 1;
-- 查看慢查询日志文件路径
SHOW VARIABLES LIKE 'slow_query_log_file';
-- 确认慢查询日志是否开启
SHOW VARIABLES LIKE 'slow_query_log';
-- 如果未开启,手动开启
SET GLOBAL slow_query_log = 'ON';
-- (可选)同时记录没有用到索引的查询——帮你发现"忘了加索引"的 SQL
SET GLOBAL log_queries_not_using_indexes = 1;
持久化配置:上面用
SET GLOBAL修改的设置在 MySQL 重启后会失效。如果要永久生效,需要改配置文件:
- Debian/Ubuntu:
/etc/mysql/mysql.conf.d/mysqld.cnf- CentOS/RHEL:
/etc/my.cnf或/etc/mysql/my.cnf- macOS (Homebrew):
/opt/homebrew/etc/my.cnf在
[mysqld]段落下添加:[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1改完后重启:
sudo systemctl restart mysql
5.3 分析慢查询日志
慢查询日志通常位于 /var/log/mysql/slow.log(路径视操作系统而定),可以用 tail -f 实时查看:
# 实时监控慢查询日志
sudo tail -f /var/log/mysql/slow.log
每条慢查询记录包含以下信息:
# Time: 2026-08-26T10:15:23.456789Z
# User@Host: root[root] @ localhost [] Id: 42
# Query_time: 3.214567 Lock_time: 0.000100 Rows_sent: 100 Rows_examined: 50000
SET timestamp=1724644523;
SELECT * FROM orders WHERE user_id = 2 AND order_time > '2026-01-01';
关键字段:
- Query_time: 查询耗时(3.21 秒——这就是慢查询!)
- Rows_examined: 扫描了 50000 行却只返回 100 行——效率极低
- Lock_time: 等待锁的时间(短则不影响,长了要关注)
5.4 用工具解析慢查询日志
MySQL 自带了一个日志分析工具 mysqldumpslow:
# 按查询次数排序,只看 Top 10
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
# 按平均耗时排序,Top 10
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# 只看含有"ORDER BY"的慢查询
mysqldumpslow -s t -t 10 -g "order by" /var/log/mysql/slow.log
对于大量慢查询的场景,也可以用 Percona 的开源工具 pt-query-digest(需要单独安装),它能生成详细的分析报告。
5.5 典型慢 SQL 优化套路
| 症状 | 原因 | 优化方法 |
|---|---|---|
type=ALL,Rows_examined >> Rows_sent | 没有合适的索引 | 根据 WHERE 条件加索引 |
Using filesort | ORDER BY 列无索引 | 给排序列加索引,或调整联合索引的顺序 |
Using temporary | GROUP BY/DISTINCT 需临时表 | 加索引减少分组范围 |
LIKE '%xxx' | 前缀通配导致索引失效 | 改用全文索引(FULLTEXT)或 Elasticsearch |
| SELECT * | 取了不需要的列 | 只查需要的列(尤其是避免取大 TEXT 列) |
六、事务与隔离级别
6.1 什么是事务?
白话:事务就是一组操作——要么全部成功,要么全部失败回滚,不存在"一半成功一半失败"的情况。
经典场景:转账。A 转 100 元给 B——先从 A 扣 100,再给 B 加 100。如果第一步成功了第二步失败了(比如断电、程序崩溃),A 的钱丢了但 B 没收到,钱就凭空消失了。事务机制保证这两步要么都完成、要么都不做。
InnoDB 存储引擎支持完整的事务,MyISAM 不支持。
6.2 ACID 四大特性
| 特性 | 含义 | 大白话 |
|---|---|---|
| 原子性(Atomicity) | 事务中的所有操作要么全部完成,要么全部不做 | “要么全活,要么全死,不会半死不活” |
| 一致性(Consistency) | 事务前后数据库的完整性约束没有被破坏 | “事务开始前和结束后,数据都是合法的” |
| 隔离性(Isolation) | 并发事务之间互不影响 | “大家各干各的,不会因为别人的操作而乱套” |
| 持久性(Durability) | 事务提交后,修改永久保存 | “提交了就不怕断电,关机重启还在” |
6.3 事务控制命令
-- 方法1:显式开启事务
START TRANSACTION;
-- 或等价写法
BEGIN;
-- 一系列操作...
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
-- 检查无误后提交(永久的写入磁盘)
COMMIT;
-- ---- 如果发现有问题,回滚(撤销所有未提交的修改)----
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- 哎呀,刚手滑多减了,赶紧撤回!
ROLLBACK;
-- 此时 user_id=1 的余额已经恢复原样了
SAVEPOINT(保存点):可以在事务中标记一个"检查点",回滚时可以选择只回到某个保存点而不回滚整个事务:
START TRANSACTION;
INSERT INTO users (username, email) VALUES ('test1', '[email protected]');
SAVEPOINT sp1; -- 打个标记
INSERT INTO users (username, email) VALUES ('test2', '[email protected]');
ROLLBACK TO SAVEPOINT sp1; -- 只回滚到 sp1,test2 被丢弃,test1 保留
-- 释放保存点(可选)
RELEASE SAVEPOINT sp1;
6.4 事务隔离级别
隔离性靠隔离级别来实现。MySQL 定义了 4 种隔离级别(从低到高):
| 级别 | 英文 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|---|
| 1 | READ UNCOMMITTED(读未提交) | ❌ 可能发生 | ❌ 可能发生 | ❌ 可能发生 |
| 2 | READ COMMITTED(读已提交) | ✅ 避免 | ❌ 可能发生 | ❌ 可能发生 |
| 3 | REPEATABLE READ(可重复读) | ✅ 避免 | ✅ 避免 | ❌ 可能发生(InnoDB 通过 MVCC 基本避免了) |
| 4 | SERIALIZABLE(串行化) | ✅ 避免 | ✅ 避免 | ✅ 避免 |
三种读取异常的解释:
| 异常 | 白话解释 | 举例 |
|---|---|---|
| 脏读(Dirty Read) | 读到别人还没提交的修改,万一对方回滚了你读的就是"脏数据" | A 改了余额从 1000→900(还没提交),B 看到 900 就去花了——结果 A 回滚了,余额还是 1000 |
| 不可重复读(Non-repeatable Read) | 同一事务内两次读同一行,结果不一样(被别人改了) | A 读到余额 1000,B 改成 900 并提交,A 再读变成 900 |
| 幻读(Phantom Read) | 同一事务内两次查同一范围,结果行数不一样(别人插入了新行) | A 查"余额>500 的用户有 5 个",B 插入了一个新用户(余额 600),A 再查变成 6 个 |
6.5 查看和设置隔离级别
-- 查看当前会话的隔离级别
SELECT @@transaction_isolation;
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 设置当前会话的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别(影响新建连接,需 SUPER 权限)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
InnoDB 默认级别是 REPEATABLE READ(可重复读),这是大多数场景的最佳选择。除非你有特殊需求,一般不需要改。
PostgreSQL 的默认级别是 READ COMMITTED——所以同样的代码从 Postgres 搬到 MySQL,某些地方的行为会有差异。如果你的项目后来换了数据库,这点要注意。
6.6 隔离级别的实用建议
| 场景 | 推荐级别 | 理由 |
|---|---|---|
| 金融/银行/支付 | SERIALIZABLE 或 REPEATABLE READ | 宁可慢也不能错 |
| 一般业务系统 | REPEATABLE READ(InnoDB 默认) | 够用、性能好 |
| 读多写少的报表/统计 | READ COMMITTED | 读到最新数据,不阻塞写 |
| 高并发读场景 | READ UNCOMMITTED(不推荐) | 几乎不用——读到脏数据的代价太高 |
软广时间:如果你在选型 API 服务的时候纠结——本地部署太麻烦,云端官方又贵,可以考虑云间 API 中转站(cloudzone-api.cyou),国内直连、OpenAI 兼容 + Anthropic 兼容、90+ 模型按量计费 0.05x 起,香港节点延迟低,集成成本跟改 BASE_URL 差不多。很多团队用它做开发环境的 API 路由。
七、总结与最佳实践清单
以下是日常写 SQL 时的速查清单,打印贴在显示器旁边随时对照:
索引类
- WHERE 条件列加索引(尤其等值查询和范围查询)
- JOIN 关联列加索引
- 联合索引遵守最左前缀原则
- 别滥用索引——写多读少的表反而会被拖累
- 定期用
ANALYZE TABLE table_name;更新索引统计信息
SQL 写法类
- 不用 SELECT *,只取需要的列
- LIKE 不用
%开头(前缀通配不走索引) - 不在索引列上做函数运算(
WHERE YEAR(order_time) = 2026会让索引失效,改为WHERE order_time >= '2026-01-01' AND order_time < '2027-01-01') - OR 连接的列至少有一个有索引
监控类
- 开慢查询日志(
long_query_time = 1) - 定期
EXPLAIN关键 SQL,确保 type 不是 ALL - 警惕 Using filesort 和 Using temporary
事务类
- 需要原子性的操作用 START TRANSACTION … COMMIT
- 保持事务尽量短——不要在事务里做 HTTP 请求等耗时操作
- 默认隔离级别 REPEATABLE READ 够用了,别随意改
八、一键安装脚本
为方便在 Linux / macOS / WSL 环境中快速搭建 MySQL 并导入示例数据,提供以下双系统脚本。
脚本功能:检测网络环境→安装 MySQL/MariaDB→初始化安全提醒→导入示例库(users/orders 表)→验证查询。
如何下载:不方便下载的同学可以直接复制下方完整源码,新建文本文档粘贴后改后缀为 .sh 或 .ps1 运行;也可从 install-mysql-guide.sh(.ps1 版同理)下载。
Linux / macOS / WSL(bash)
#!/usr/bin/env bash
set -u
# ============================================================
# MySQL 索引与 SQL 优化实战 — 一键安装脚本(macOS / Linux / WSL)
# 公开源码,欢迎审查 —— 不放心可先复制给 AI 判断
# ============================================================
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
step() { echo -e "${CYAN}[STEP]${NC} $1"; }
detect_network() {
step "检测网络环境(国内 / 国外)..."
if curl -fsI --max-time 5 "https://claude.ai" >/dev/null 2>&1; then
info "可直连 claude.ai,判定为海外网络"
echo "overseas"
else
warn "无法直连 claude.ai,判定为国内网络环境(将使用国内镜像源)"
echo "domestic"
fi
}
get_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "$ID"
elif command -v sw_vers >/dev/null 2>&1; then
echo "darwin"
else
echo "unknown"
fi
}
detect_os() {
step "检测操作系统..."
local os_id
os_id=$(get_os)
if [ "$os_id" = "darwin" ]; then
info "检测到 macOS"
echo "macos"
elif [ "$os_id" = "debian" ] || [ "$os_id" = "ubuntu" ]; then
info "检测到 Debian / Ubuntu"
echo "debian"
elif [ "$os_id" = "centos" ] || [ "$os_id" = "rhel" ] || [ "$os_id" = "fedora" ]; then
info "检测到 CentOS / RHEL / Fedora"
echo "redhat"
elif [ "$os_id" = "alpine" ]; then
info "检测到 Alpine Linux"
echo "alpine"
else
warn "未知发行版,尝试使用 apt 安装"
echo "debian"
fi
}
check_mysql_running() {
if command -v mysql >/dev/null 2>&1; then
if mysql -u root -e "SELECT 1;" >/dev/null 2>&1; then
info "MySQL 已安装并正在运行"
return 0
fi
fi
return 1
}
install_on_debian() {
local net="$1"
step "安装 MySQL Server(Debian/Ubuntu 系)..."
if [ "$net" = "domestic" ]; then
warn "国内网络,尝试使用 apt 默认源(如超时请手动更换清华源)..."
fi
if sudo apt-get update --allow-releaseinfo-change -o Dir::Etc::sourcelist="null" -o Dir::Etc::sourceparts="-" >/dev/null 2>&1 || true; then
:
fi
if sudo DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server >/dev/null 2>&1; then
info "MySQL 安装成功"
return 0
fi
warn "apt 安装失败,尝试 MariaDB(MySQL 的兼容替代品,功能一致)..."
if sudo DEBIAN_FRONTEND=noninteractive apt-get install -y mariadb-server >/dev/null 2>&1; then
info "MariaDB 安装成功(功能与 MySQL 高度兼容)"
return 0
fi
error "MySQL/MariaDB 安装失败。"
echo " 请手动尝试:sudo apt install -y mysql-server"
echo " 或换清华源后重试:sudo sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list"
return 1
}
install_on_redhat() {
step "安装 MySQL Server(CentOS/RHEL 系)..."
if sudo dnf install -y mysql-server 2>/dev/null; then
info "MySQL 安装成功"
return 0
fi
warn "dnf 安装失败,尝试 yum..."
if sudo yum install -y mysql-server 2>/dev/null; then
info "MySQL 安装成功(yum)"
return 0
fi
warn "MySQL 安装失败,尝试 MariaDB..."
if sudo dnf install -y mariadb-server 2>/dev/null; then
info "MariaDB 安装成功"
return 0
fi
error "MySQL/MariaDB 安装失败。请手动安装:sudo dnf install -y mysql-server"
return 1
}
install_on_macos() {
step "安装 MySQL(macOS)..."
if command -v brew >/dev/null 2>&1; then
if brew install mysql >/dev/null 2>&1; then
info "MySQL 安装成功"
brew services start mysql >/dev/null 2>&1
return 0
fi
fi
error "Homebrew 不可用或安装失败。"
echo " 请先安装 Homebrew:/bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
echo " 或使用 ghfast.top 代理加速安装脚本(国内):"
echo " /bin/bash -c \"\$(curl -fsSL https://ghfast.top/https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
return 1
}
install_on_alpine() {
step "安装 MySQL/MariaDB(Alpine Linux)..."
if apk add --no-cache mariadb mariadb-server >/dev/null 2>&1; then
info "MariaDB 安装成功"
return 0
fi
error "Alpine 安装失败。请手动:apk add --no-cache mariadb mariadb-server && rc-update add mariadbd && rc-start mariadbd"
return 1
}
start_mysql() {
step "启动 MySQL 服务..."
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl enable mysql 2>/dev/null || sudo systemctl enable mysqld 2>/dev/null || true
sudo systemctl start mysql 2>/dev/null || sudo systemctl start mysqld 2>/dev/null || true
elif command -v brew >/dev/null 2>&1; then
brew services start mysql 2>/dev/null || true
fi
sleep 2
if command -v mysql >/dev/null 2>&1 && mysql -u root -e "SELECT 1;" >/dev/null 2>&1; then
info "MySQL 服务已启动"
return 0
fi
warn "MySQL 服务未能自动启动,尝试直接初始化..."
if command -v mysqld >/dev/null 2>&1; then
sudo mysqld_safe --skip-grant-tables &>/dev/null &
sleep 3
if mysql -u root -e "SELECT 1;" >/dev/null 2>&1; then
info "MySQL 通过 mysqld_safe 启动成功"
return 0
fi
fi
warn "MySQL 可能已经运行或需要你手动启动。"
echo " 手动启动方式:"
echo " systemd : sudo systemctl start mysql (Debian/Ubuntu) 或 sudo systemctl start mysqld (CentOS)"
echo " Homebrew: brew services start mysql"
echo " 其他 : sudo service mysql start"
return 0
}
setup_security() {
warn "安全提醒:"
echo " 生产环境请务必运行以下命令进行安全加固:"
echo " sudo mysql_secure_installation"
echo " 这将引导你设置 root 密码、删除匿名用户、禁止 root 远程登录。"
echo " 如果需要免密本地管理,也可以先跳过并在必要时再配置。"
}
create_demo_database() {
step "创建示例数据库 shop 并导入测试数据..."
# 等待 MySQL 完全就绪
local retries=10
while ! mysql -u root -e "SELECT 1;" >/dev/null 2>&1; do
retries=$((retries - 1))
if [ "$retries" -le 0 ]; then
error "MySQL 仍未响应,无法创建数据库。"
return 1
fi
info "等待 MySQL 就绪...(剩余 ${retries} 次尝试)"
sleep 2
done
mysql -u root <<'SQLEOF'
CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE shop;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0,
category VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
order_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT
) ENGINE=InnoDB;
INSERT INTO users (username, email) VALUES
('alice', '[email protected]'),
('bob', '[email protected]'),
('charlie', '[email protected]');
INSERT INTO products (name, price, stock, category) VALUES
('机械键盘', 399.00, 50, '数码'),
('无线鼠标', 89.90, 200, '数码'),
('Python 入门教程', 49.00, 1000, '图书'),
('咖啡杯', 35.00, 300, '生活');
INSERT INTO orders (user_id, product_id, quantity) VALUES
(1, 1, 1),
(1, 3, 2),
(2, 2, 3);
CREATE INDEX idx_username ON users(username);
CREATE INDEX idx_category ON products(category);
SELECT 'Demo data inserted successfully!' AS status;
SQLEOF
if [ $? -eq 0 ]; then
info "示例数据库创建完成!"
return 0
else
error "示例数据导入失败"
return 1
fi
}
verify_query() {
step "验证安装——运行联合查询测试..."
mysql -u root -D shop -e "
SELECT u.username, p.name, o.quantity, o.order_time
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id
WHERE u.username = 'alice';
" 2>/dev/null
if [ $? -eq 0 ]; then
info "查询验证通过!"
return 0
else
error "查询验证失败"
return 1
fi
}
main() {
echo "============================================"
echo " MySQL 索引与 SQL 优化实战 — 一键安装脚本"
echo " 适用于 macOS / Linux / WSL"
echo "============================================"
echo ""
local NET
NET=$(detect_network)
local OS_TYPE
OS_TYPE=$(detect_os)
if check_mysql_running; then
info "MySQL 已在运行,跳过安装步骤"
else
case "$OS_TYPE" in
debian) install_on_debian "$NET" || exit 1 ;;
redhat) install_on_redhat ;;
macos) install_on_macos ;;
alpine) install_on_alpine ;;
*) warn "未知系统,尝试通用安装流程"; install_on_debian "$NET" || exit 1 ;;
esac
fi
start_mysql
setup_security
create_demo_database
verify_query
echo ""
echo "============================================"
info "安装完成!"
echo " 进入数据库:mysql -u root -p"
echo " 选择示例库:USE shop;"
echo " 查看用户表:SELECT * FROM users;"
echo " 查看索引: SHOW INDEX FROM users;"
echo " 查看执行计划:EXPLAIN SELECT * FROM users WHERE username = 'alice';"
echo ""
echo " 后续请跑:sudo mysql_secure_installation 加固安全"
echo "============================================"
}
main "$@"
Windows PowerShell(WSL 引导器)
# ============================================================
# MySQL 索引与 SQL 优化实战 — 一键安装脚本(Windows PowerShell)
# 公开源码,欢迎审查 -- 不放心可先复制给 AI 判断
# 说明:MySQL 官方不支持 Windows 原生安装脚本分发,
# 本脚本调用 WSL 内的 Ubuntu 执行 Linux 版安装逻辑。
# 如果你没有 WSL,也可去 https://www.mysql.com/downloads/
# 下载安装包(Windows MSI 版)手动安装。
# ============================================================
$ErrorActionPreference = "Stop"
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green }
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Err { Write-Host "[ERROR] $args" -ForegroundColor Red }
function Write-Step { Write-Host "[STEP] $args" -ForegroundColor Cyan }
# 1. 安装策略:仅当前会话生效(Process 级别),关闭窗口即恢复,不动系统全局策略
Write-Step "准备执行环境..."
# 2. 检测 WSL
Write-Step "检查 WSL 环境..."
if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
Write-Err "未找到 wsl 命令。请先安装 WSL:"
Write-Host " PowerShell(管理员)执行:wsl --install"
Write-Host " 重启后安装 Ubuntu 发行版,再运行本脚本。"
Write-Host ""
Write-Host "如果不想用 WSL,也可以直接从官网下载 MySQL Installer:"
Write-Host " https://dev.mysql.com/downloads/installer/"
Write-Host " (勾选 ghfast.top 代理加速:https://ghfast.top/https://dev.mysql.com/downloads/installer/ )"
exit 1
}
try {
$wslDistros = wsl -l -q 2>$null
if ($wslDistros -match "Ubuntu") {
$wslDistro = "Ubuntu"
} elseif ($wslDistros -match "Ubuntu-22.04") {
$wslDistro = "Ubuntu-22.04"
} elseif ($wslDistros -match "Ubuntu-20.04") {
$wslDistro = "Ubuntu-20.04"
} else {
$firstDistro = ($wslDistros | Where-Object { $_.Trim() -and $_.Trim() -ne "CONTAINERS" -and $_.Trim() -ne "STATUS" -and $_.Trim() -ne "NAME" } | Select-Object -First 1).Trim()
if ([string]::IsNullOrWhiteSpace($firstDistro)) {
Write-Err "WSL 中未检测到可用的 Linux 发行版。请先运行:wsl --install"
exit 1
}
$wslDistro = $firstDistro
}
} catch {
$wslDistro = "Ubuntu"
}
Write-Info "使用 WSL 发行版:$wslDistro"
# 3. 询问是否继续
Write-Host ""
Write-Host "本脚本将在 WSL($wslDistro)中自动安装 MySQL 并导入示例数据。"
Write-Host "整个过程大约需要 3-10 分钟(取决于网速)。"
Write-Host "你也可以选择仅在本机安装 MySQL(通过官网安装包)而非在此脚本运行。"
$continue = Read-Host "继续在 WSL 中安装 MySQL?[Y/N]"
if ($continue -ne "Y" -and $continue -ne "y") {
Write-Info "已选择跳过。需要时重新运行本脚本即可。"
exit 0
}
# 4. 把 Linux 脚本注入 WSL 并执行
Write-Step "将安装脚本传入 WSL 并执行(首次执行会安装 MySQL,耗时较长)..."
$scriptPath = Join-Path $PSScriptRoot "install-mysql-guide.sh"
if (-not (Test-Path $scriptPath)) {
Write-Warn "未找到同目录的 install-mysql-guide.sh,请将本 .ps1 与 install-mysql-guide.sh 放在同一目录后重跑。"
Write-Host "(或者手动在 WSL 中下载脚本执行:)"
Write-Host " wsl -d $wslDistro -e bash -c ""curl -fsSL https://cleanresolver.com/scripts/install-mysql-guide.sh -o /tmp/install-mysql-guide.sh && bash /tmp/install-mysql-guide.sh"""
exit 1
}
$remotePath = "/tmp/install-mysql-guide.sh"
$tempWindows = [System.IO.Path]::GetFullPath($env:TEMP)
$mntPath = "/mnt/c" + $tempWindows -replace '^C:', '' -replace '\\', '/'
$localScript = "$mntPath/install-mysql-guide-wsl.sh"
try {
Copy-Item -Path $scriptPath -Destination $localScript -Force
Write-Info "脚本已传入 WSL:$localScript"
} catch {
Write-Err "传入 WSL 失败:$_"
Write-Host "请手动在 WSL 中执行:bash <(curl -fsSL https://cleanresolver.com/scripts/install-mysql-guide.sh)"
exit 1
}
Write-Host ""
Write-Host "正在 WSL 内执行安装(日志将直接输出)……"
Write-Host "============================================"
wsl -d $wslDistro -e bash $localScript
$code = $LASTEXITCODE
Write-Host "============================================"
if ($code -ne 0) {
Write-Err "WSL 内安装失败(退出码 $code)。请对照上方报错处理,或手动在 WSL 中重试。"
exit 1
}
Write-Host ""
Write-Host "============================================"
Write-Info "全部完成!接下来在 WSL 终端里:"
Write-Host " mysql -u root -p # 进入数据库"
Write-Host " USE shop; # 选择示例库"
Write-Host " SELECT * FROM users; # 查看用户表"
Write-Host " EXPLAIN SELECT * FROM users WHERE username='alice'; # 查看执行计划"
Write-Host ""
Write-Host "Windows 浏览器可通过 localhost:3306 连接 MySQL(需配置端口转发)。"
Write-Host "============================================"
九、扩展阅读
- runoob MySQL 系列教程:https://www.runoob.com/mysql/mysql-tutorial.html
- MySQL 官方文档(中文):https://dev.mysql.com/doc/refman/8.0/
- 阿里云 MySQL 官方文档镜像(国内访问更快):https://help.aliyun.com/document_detail/26599.html
- Percona pt-query-digest 慢查询分析工具:https://www.percona.com/doc/percona-toolkit/LATEST/pt-query-digest.html
- MySQL 隔离级别深入:https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-model.html
最后提醒:所有脚本均为公开源码,你可以在文末看到完整内容,放心复制审查。如果有任何问题,欢迎在评论区留言讨论——不过记得评论前先过 Turnstile 验证(防机器人机制),每篇文章有自己的冷却时间哦。
请完成验证后查看评论区