导读

你是否遇到过这种场景:一条 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+ 树的核心特征:

  1. 所有数据都存在叶节点(Leaf Node)。非叶节点(Interior Node / 根节点)只存键值和指向子节点的指针——相当于目录只有章节名和页码,正文内容全在书中。
  2. 叶节点之间用链表连接。这使得范围查询(WHERE age BETWEEN 20 AND 30)不需要回退到根节点重新搜索,沿着叶链表一路扫过去就行。
  3. 非叶节点只存键,不存完整数据行。这意味着单个磁盘页能放下更多键值,树的高度更矮。
  4. 多路平衡。普通二叉树可能退化成链表(最坏情况查 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 > 20WHERE 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 会变成 refconst(取决于是否有唯一索引),rows 通常只会是 1。

实用技巧:想让 SQL 写得不好的人(包括我自己)少踩坑,可以让 AI 帮你提前审一遍——比如拿云间中转站的模型(cloudzone-api.cyou)快速跑一下"这条 SQL 会不会慢”,零门槛,几十块钱就能搞定一个月的用量。

4.5 Extra 中的警告信号

含义如何处理
Using filesortMySQL 需要在内存或磁盘中做一次额外的排序(没用到索引排序)检查 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=ALLRows_examined >> Rows_sent没有合适的索引根据 WHERE 条件加索引
Using filesortORDER BY 列无索引给排序列加索引,或调整联合索引的顺序
Using temporaryGROUP 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 种隔离级别(从低到高):

级别英文脏读不可重复读幻读
1READ UNCOMMITTED(读未提交)❌ 可能发生❌ 可能发生❌ 可能发生
2READ COMMITTED(读已提交)✅ 避免❌ 可能发生❌ 可能发生
3REPEATABLE READ(可重复读)✅ 避免✅ 避免❌ 可能发生(InnoDB 通过 MVCC 基本避免了)
4SERIALIZABLE(串行化)✅ 避免✅ 避免✅ 避免

三种读取异常的解释:

异常白话解释举例
脏读(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 验证(防机器人机制),每篇文章有自己的冷却时间哦。