开篇不废话:你写的SQL,数据库可能真的在“骂娘”
我见过不少新手,一上来就甩出一堆“优雅”的查询,结果数据库慢得像乌龟爬山。
也见过老司机,明明建了索引,却因为一个函数调用,让索引直接“下岗”。
今天咱们不聊那些玄乎的“架构师心法”,就干两件实在事:索引优化 和 事务隔离级别。
我会手敲几个小demo,你跟着跑一遍,比看十篇“最佳实践”都管用。
一、索引优化:别让数据库做“全文扫描式苦力”
1.1 先看一个“索引失效”的翻车现场
假设你有张用户表:
sql
CREATE TABLE user (
id int NOT NULL AUTO_INCREMENT,
phone varchar(20) NOT NULL,
create_time datetime NOT NULL,
PRIMARY KEY (id),
KEY idx_phone (phone)
);
你想查手机号后四位是 1234 的用户,顺手写了句:
sql
SELECT * FROM user WHERE RIGHT(phone, 4) = '1234';
先别跑,咱们用 EXPLAIN 偷看一眼执行计划:
sql
EXPLAIN SELECT * FROM user WHERE RIGHT(phone, 4) = '1234';
你猜 type 列是什么?大概率是 ALL——全表扫描。
因为 RIGHT(phone,4) 这个函数,让MySQL没法用 idx_phone 索引。
索引只认“原汁原味”的列值,你一套上函数,它立马懵逼。
怎么改?
换个思路:用 LIKE 配合通配符,但注意别把通配符放开头(那样也会失效)。
sql
-- 正确姿势:把计算挪到右边
SELECT * FROM user WHERE phone LIKE '%1234';
等等,LIKE '%1234' 其实也走不了索引!因为通配符在最前面。
那咋办?老老实实加一个存储生成列(MySQL 5.7+ 支持):
sql
ALTER TABLE user ADD COLUMN phone_last4 VARCHAR(4)
GENERATED ALWAYS AS (RIGHT(phone,4)) STORED,
ADD KEY idx_phone_last4 (phone_last4);
然后查询改为:
sql
SELECT * FROM user WHERE phone_last4 = '1234';
这时候 EXPLAIN 再看,type 会变成 ref,爽快。
1.2 另一个常见坑:隐式类型转换
sql
-- phone 字段是 VARCHAR,但传了个整数进去
EXPLAIN SELECT * FROM user WHERE phone = 13800138000;
MySQL 会偷偷把 phone 转成数字再比较,导致索引失效。
记住一条铁律:字段类型和查询条件类型保持一致。
二、事务隔离级别:别让数据“闹鬼”
你在代码里开了一个事务,查余额是100块,然后另一笔交易把它改成50。
等你的事务再去取余额,到底该看到100还是50?
这就是隔离级别要解决的问题。
2.1 先搭一个测试表
sql
CREATE TABLE account (
id int NOT NULL,
balance int NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO account VALUES (1, 100);
2.2 演示“脏读”:读未提交
打开两个MySQL命令行窗口(会话A和会话B)。
会话A(设置隔离级别为读未提交,并开始事务但不提交):
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE account SET balance = 50 WHERE id = 1;
-- 注意:这里还没 COMMIT
会话B(同样设为读未提交,然后去查):
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT balance FROM account WHERE id = 1;
你会看到 50。但这时如果会话A 回滚(ROLLBACK),这个50就根本没发生过——你读到了“脏数据”。
生产环境里,这种错误会导致统计、转账一塌糊涂。
2.3 演示“幻读”:可重复读下插入新行
MySQL默认隔离级别是 REPEATABLE READ(可重复读),它能避免脏读和不可重复读,但躲不掉幻读。
会话A:
sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
-- 第一次查:没有 id=3 的用户
SELECT * FROM account WHERE id = 3;
会话B(正常自动提交):
sql
INSERT INTO account VALUES (3, 200);
COMMIT;
会话A再次执行:
sql
SELECT * FROM account WHERE id = 3;
你依然看不到 id=3 —— 可重复读保证了同一事务内多次查询结果一致,这没问题。
但如果你在会话A里想更新 id=3:
sql
UPDATE account SET balance = balance + 10 WHERE id = 3;
咦?更新成功了!然后你再一次 SELECT,那条“鬼魂”一样的 id=3 突然冒出来了。
这就是幻读:你在事务里以为不存在的一行,其实已被别人插入,而你自己的更新却“看见”了它。
怎么解决?
用 SERIALIZABLE 隔离级别(性能损耗大)或者带上 SELECT ... FOR UPDATE 加锁。
三、给你三道训练题(自己跑一遍)
1、 索引失效排查
对一张十万行的订单表执行 EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2025-01-01';
试试改成 create_time BETWEEN '2025-01-01 00:00:00' AND '2025-01-01 23:59:59',对比 type 列的变化。
2、 脏读实战
自己开两个会话,用 READ UNCOMMITTED 演示一次更新后回滚,确认另一个会话读到脏数据。
3、 幻读重现
按上面的 account 表,严格按照“事务A查不到某行 → 事务B插入该行 → 事务A更新该行 → 再次查询看到新行”的步骤,记录下每个步骤的结果。
写在最后:别背“口诀”,去跑“真车”
很多人喜欢收藏什么“索引优化十条”“隔离级别七宗罪”,但一遇到真实慢查询,还是手忙脚乱。
我的建议很朴素:每个新知识点,花5分钟写个可复现的SQL,用EXPLAIN和两个终端窗口反复折腾。当然如果你也可以到重庆思庄这样做数据库运维十余年经验来学习和探讨,或许会让你收获更多知识。
你折腾得越丑,理解得越深。
下次再有人跟你说“数据库很慢”,你可以回一句:“先跑个EXPLAIN,看看索引有没有摸鱼。”
——这句话,比一百遍“首先其次”都管用。
以上所有示例基于 MySQL 5.7+/8.0,已在个人测试环境验证。
无任何推测内容。如需官方手册,请参考 MySQL Reference Manual (Oracle, 2023)。