每日一讲技术题(7.29-30)

每日一讲技术题(7.29-30)

第一问

什么是锁?InnoDB 有哪些行级锁类型(共享锁、排他锁)?意向锁的作用是什么?

一、什么是锁?

锁是数据库用来控制并发访问的机制,目的是保证数据在多人同时读写时的正确性。

打个比方:你爸去取款机取钱,同时你妈也在手机银行转账,两个人的操作针对同一个账户。如果没有锁,可能余额扣了两次、或者明明余额不够却取出来了。

锁的本质就是一句话:让冲突的操作排队执行


二、InnoDB 的行级锁类型

InnoDB 的三种基本行级锁:

  1. Record Lock(记录锁)

锁住索引记录本身(不是行,是索引记录)。

-- 对 id=5 的行加锁
SELECT * FROM user WHERE id = 5 FOR UPDATE;
  • 如果 id 是主键,锁的是主键索引上 id=5 的那条记录
  • 如果 id 是唯一索引,锁的也是唯一索引上的那条记录
  1. Gap Lock(间隙锁)

锁住两条索引记录之间的间隙,防止其他事务在这个间隙插入新记录。

索引值:1, 3, 5, 8, 10
间隙:(-∞, 1), (1, 3), (3, 5), (5, 8), (8, 10), (10, +∞)
-- 间隙锁作用:其他事务插入 id=4 会被阻塞
SELECT * FROM user WHERE id BETWEEN 3 AND 5 FOR UPDATE;

Gap Lock 的存在是 MySQL RR 隔离级别能防幻读的根本原因

  1. Next-Key Lock(临键锁)

Record Lock + Gap Lock 的组合,锁住"当前记录"+"前面的间隙"。

索引值:1, 3, 5
Next-Key Lock 覆盖的区间:
  (-∞, 1]  → 锁住 1 及其前面的间隙
  (1, 3]   → 锁住 3 及其与 1 之间的间隙
  (3, 5]   → 锁住 5 及其与 3 之间的间隙
  (5, +∞)  → 锁住 5 之后的间隙
-- 加锁区间是 (3, 5]
-- 即:锁住 id=5 这行,同时锁住 3 到 5 之间的间隙
SELECT * FROM user WHERE id = 5 FOR UPDATE;

注意:如果 id=5 这条记录不存在,就只变成 Gap Lock,锁住间隙但不锁具体记录。

  1. Insert Intention Lock(插入意向锁)

一种特殊的 Gap Lock(间隙锁),用于 INSERT 操作。

  • 多个事务可以同时持有一个间隙的插入意向锁(互相兼容)
  • 但如果有事务在这个间隙有 Gap Lock,插入意向锁必须等
-- 事务A:锁住间隙 (3, 5)
SELECT * FROM user WHERE id = 4 FOR UPDATE;

-- 事务B:想插入 id=4,必须等事务A释放 → 被阻塞
INSERT INTO user(id) VALUES(4);

这样设计是为了在防幻读的同时,尽量让插入操作并行化(不冲突的插入可以同时进行)。


三、意向锁的作用

意向锁是表级别的锁,分两种:

含义
IS(意向共享锁) 表示"这个事务准备在表里的某些行上加共享锁(S)"
IX(意向排他锁) 表示"这个事务准备在表里的某些行上加排他锁(X)"

核心作用:快速检测表级锁冲突

假设没有意向锁:

事务A:给 user 表的 id=1 这行加了 X 锁
事务B:想给整张 user 表加 LOCK TABLES WRITE

如果没有意向锁,事务B必须遍历整张表的每一行检查有没有行锁,才能确定能不能加表锁。一张千万级的大表,这判断就是灾难。

有了意向锁:

事务A 加行锁时,会在表上留下一把 IX 锁
事务B 想加表锁时,直接检查有没有 IX/IS 锁
  → 有 IX 锁 → 说明有行级排他锁 → 直接阻塞,不用遍历行

一句话:意向锁让表级锁和行级锁的冲突判断从 O(n) 变成 O(1)。

兼容矩阵

已持有 ↓ 请求 → IS IX S X
IS IS 兼容 兼容 兼容 不兼容
IX IX 兼容 兼容 不兼容 不兼容
S S 兼容 不兼容 兼容 不兼容
X X 不兼容 不兼容 不兼容 不兼容

兼容 = 两个锁可以同时存在
不兼容 = 后请求的事务必须等待,直到先持有的锁释放

  • IX 和 IX 之间兼容 — 两个事务可以分别锁不同的行,互不影响
  • IX 和 S 之间不兼容 — 已经有行锁了,不能再加共享表锁
  • IX 和 IS 之间兼容 — 都只是在"准备"阶段,不冲突

四、总结

概念 一句话
让冲突操作排队,保证数据一致性
Record Lock 锁住某条记录本身
Gap Lock 锁住记录之间的空隙,防止插入
Next-Key Lock 记录+间隙组合锁,RR 级别防幻读
Insert Intention Lock 插入时用的特殊间隙锁,允许并行插入
意向锁(IS/IX) 表级的"标记",快速判断能不能加表锁,避免扫全表检测行锁

第二问

什么是死锁?请描述一个MySQL中产生死锁的场景,以及如何避免和解决死锁。

一、什么是死锁

死锁 是指两个或多个事务互相持有对方需要的资源(锁),并且互相等待对方释放,导致谁都无法继续执行的情况。

MySQL 不会让它们永远等下去——它会自动检测死锁,选择一个开销较小的事务作为"牺牲品"回滚掉,让另一个事务正常执行。被回滚的事务会收到 Deadlock found when trying to get lock 错误。


二、典型场景

假设有两个事务 T1 和 T2:

时间 T1 T2
1 UPDATE accounts SET balance = balance - 100 WHERE id = 1;
持有 id=1 的行锁
2 UPDATE accounts SET balance = balance - 200 WHERE id = 2;
持有 id=2 的行锁
3 UPDATE accounts SET balance = balance + 100 WHERE id = 2;
等待 T2 释放 id=2 的锁 — 阻塞
4 UPDATE accounts SET balance = balance + 200 WHERE id = 1;
等待 T1 释放 id=1 的锁 — 阻塞

此时双方各执一锁、互等一锁,形成循环等待,死锁


三、如何避免

  1. 统一资源访问顺序 — 不管是哪个事务,操作记录都按 id 从小到大访问。上面的例子只要约定"先更新 id 小的,再更新 id 大的",死锁就不可能出现。

  2. 缩短事务时间 — 不要在事务里做外部 API 调用、用户输入等待这些操作,锁持的时间越短,冲突概率越低。

  3. 合理设置隔离级别 — 比如 READ COMMITTED 比 REPEATABLE READ 产生间隙锁(Gap Lock)的机会更少,死锁概率也相应降低。当然要结合业务需求来选。

  4. 使用低冲突索引 — 走全表扫描时 MySQL 会锁大量行甚至 Gap 锁,尽量让 UPDATE/DELETE 走精确的索引。


四、如何解决

  • 捕获重试 — 在应用层捕获 ER_LOCK_DEADLOCK(错误码 1213),等一小段时间后重试整个事务。因为 MySQL 已经替你选了牺牲品回滚,重试通常就能成功。
  • 查看 InnoDB 状态 — 用 SHOW ENGINE INNODB STATUS\G 查看最近一次死锁的信息,分析是哪两个事务、什么 SQL、什么索引导致的死锁。
  • 调整事务隔离级别 — 如果业务允许,考虑降到 READ COMMITTED 以减少 Gap Lock 竞争。
  • 加上合理的索引 — 确保 WHERE 条件能精准定位少量记录。

总结:

死锁就是两个事务互相等锁。避免的关键是统一定好资源访问顺序,解决的关键是捕获 + 重试 + 分析原因加索引。


第三问

数据库的三范式是什么?请分别解释并举例。什么情况下会反范式设计?

一、数据库三范式

三范式是关系数据库设计中用来规范表结构的规则,目的就两个:减少数据冗余,避免增删改时的异常。


第一范式:列不可再分

每一列必须存原子值,也就是单个不可再分的值,不允许存列表、数组或逗号分隔的一串数据。

违反的例子

学号 姓名 所选课程
001 张三 数学, 英语, 物理

"所选课程"里存了多个课程名。你想查选了数学的学生,得用模糊匹配或者字符串拆分,效率很低。

改造后

学号 姓名 课程
001 张三 数学
001 张三 英语
001 张三 物理

每个单元格只有一个值,满足第一范式。但新的问题出现了:张三的姓名重复了三次,改名字要改三行。


第二范式:消除部分依赖

前提是满足第一范式。如果表的主键是联合主键(多列组成),那么每个非主键列必须完全依赖全部主键,不能只依赖其中一部分。

违反的例子

主键是(学号, 课程)。

学号 课程 姓名 成绩
001 数学 张三 85
001 英语 张三 90

分析依赖关系:

  • 成绩:需要同时知道哪个学生的哪门课,完全依赖全部主键。
  • 姓名:只需要学号就能确定,不依赖课程。这是部分依赖。

后果:姓名重复,改名字要改多行,新增一个没选课的学生插不进去。

改造后

拆成两张表。

学生表:(学号, 姓名)

选课表:(学号, 课程, 成绩)

姓名只存一份,改一次就行。满足第二范式。


第三范式:消除传递依赖

前提是满足第二范式。非主键列必须直接依赖于主键,不能通过其他非主键列间接依赖。

这个间接依赖叫传递依赖:主键决定 A 列,A 列决定 B 列,B 列就通过 A 列间接依赖主键。

违反的例子

学生表里加了班级编号和班级名称。

学号 姓名 班级编号 班级名称
001 张三 C01 计科一班
002 李四 C01 计科一班

依赖关系:学号决定班级编号,班级编号决定班级名称。班级名称不直接依赖学号,而是通过班级编号传递过来的。

后果:计科一班的名称存了两遍。如果班级改名,得改两行。如果这个班的学生全毕业了,班级信息也跟着丢了。

改造后

拆成两张表。

学生表:(学号, 姓名, 班级编号)

班级表:(班级编号, 班级名称)

班级名称只存一次,改一次就行。满足第三范式。


补充:BCNF(巴克斯范式)

第三范式的加强版。核心区别:第三范式允许非主键列决定另一个非主键列,但 BCNF 不允许——任何能决定其他列的列,本身必须是候选键。实际业务中大部分符合第三范式的表也自然符合 BCNF,知道有这个加强版就行。


二、什么时候反范式

反范式就是故意违反范式规则,在表里引入冗余字段。目的是用空间换时间,减少 JOIN,提高读取性能。

常见场景

查询性能优化。订单列表要展示用户名、商品名、地址,按第三范式要 JOIN 五六张表,数据库压力大。在订单表里直接冗余存上用户名,一次查询搞定,省掉那次 JOIN。

高并发读。商品详情页每秒几千次请求,每次都 JOIN 扛不住。文章表里直接存作者名,排行榜表里直接存用户头像,这些在高并发系统中很常见。

分库分表后。订单数据和用户数据分到了不同数据库,没法跨库 JOIN。只能把用户昵称、手机号直接存到订单表里。

数据仓库。分析查询不关心写入一致性,追求的是聚合查询跑得快。宽表设计里大量字段冗余,这是正常的建模方式。

反范式的代价

写操作变复杂。用户改了昵称,用户表要更新,订单表、评论表、消息表里冗余的昵称也得跟着更新。

数据可能不一致。程序有 bug 或者消息队列积压,新旧数据同时存在。

存储变大。不过现在磁盘便宜,这个代价通常可以接受。

实际做法

先按第三范式设计核心业务表,保证数据一致性和写入的正确性。上线后用慢查询定位真正慢的查询,在关键路径上选择性加一两个冗余字段。同时用触发器或应用层的事件机制保证冗余字段同步更新。不提前优化,也不完全放弃范式,在写一致性和读性能之间找到适合自己的平衡点。

第四问

什么是数据库的隔离级别?MySQL的默认隔离级别是什么?它解决了哪些并发问题,又可能带来什么问题?

数据库隔离级别

隔离级别控制一个事务对数据的修改在什么时候、什么条件下能被其他事务看到。它是数据库事务四大特性里隔离性的具体实现。

SQL 标准定义了四个级别,从低到高:

第一是读未提交。一个事务可以读到另一个事务未提交的数据。性能最好,但所有并发问题都可能出现。

第二是读已提交。一个事务只能读到其他事务已提交的数据。解决了脏读,但不可重复读和幻读仍然可能。

第三是可重复读。同一个事务内,多次读取同一行数据结果一致。解决了脏读和不可重复读,但幻读在标准定义里仍然可能。

第四是串行化。事务完全串行执行,等价于排队。解决了所有并发问题,但性能最差。

三种并发问题

脏读:一个事务读到另一个事务还没提交的数据。如果对方回滚了,你读到的就是废弃的数据。

不可重复读:同一个事务里,两次读同一行数据,结果不一样,因为被其他事务修改并提交了。

幻读:同一个事务里,两次执行同一个范围查询,结果集的行数不一样,因为被其他事务插入了新行。

MySQL 的默认隔离级别

MySQL 的 InnoDB 引擎默认使用可重复读。

这里有个有意思的区别。Oracle 和 PostgreSQL 的默认级别都是读已提交,但 MySQL 选择了可重复读。而且 MySQL 在这个级别下通过两种机制做了一些超出标准定义的事情。

第一种是多版本并发控制,也就是 MVCC。每个事务看到的是数据在某个时间点的快照,同一个事务内多次读到的快照一致。

第二种是 Next Key Lock,也就是行锁加间隙锁。在范围查询时,不仅锁住匹配的行,还锁住这些行之间的间隙,阻止其他事务在这个范围内插入新数据。

这两个机制合在一起,让 MySQL 的可重复读实际上也解决了幻读问题。

它解决了哪些问题

脏读被解决了。MVCC 保证事务只能读到已提交的快照版本。

不可重复读被解决了。同一个事务内,快照一致,多次读同一行结果不变。

幻读也被解决了。Next Key Lock 在范围查询时锁住间隙,阻止其他事务插入满足条件的新行。

所以 MySQL 的可重复读提供了接近串行化的一致性保证,但并发性能比串行化好得多。

可能带来什么问题

第一个是间隙锁带来的死锁风险。这是双刃剑。范围查询时锁住不存在的行,并发插入时容易被阻塞或产生死锁。比如事务 A 锁住 id 大于 10 的所有行和间隙,事务 B 要插入 id 为 15 的行就会被阻塞。如果换成读已提交级别就不存在这个问题,因为读已提交只有行锁没有间隙锁。

第二个是并发写入性能下降。在高并发写入的场景下,间隙锁会成为瓶颈,尤其是自增列或者有唯一索引的范围更新操作。

第三个是锁的开销更大。相比读已提交,可重复读需要维护更多的锁信息,热点表上的锁争用更严重。

第四个是 Undo Log 膨胀。MVCC 需要保留 Undo Log 来构造历史快照。如果一个事务长时间不结束,这些 Undo Log 就不能被清理,导致 Undo 表空间膨胀,查询性能也可能下降,因为回滚段链变长了。

第五个是对主从复制的要求。可重复读级别下才能安全使用基于语句的 binlog 格式。因为行锁加间隙锁保证了从库执行相同 SQL 的结果和主库一致。如果降到读已提交级别,binlog 必须用行格式,否则主从数据会不一致。

总结

MySQL 默认的可重复读通过 MVCC 和 Next Key Lock 同时解决了脏读、不可重复读和幻读三个问题,比标准定义更强。代价是间隙锁带来的死锁风险和并发瓶颈。如果你的业务对幻读不敏感,写入并发又高,主动降到读已提交级别反而是更好的选择。

第五问

描述“脏读”、“不可重复读”和“幻读”的现象,并分别说明在哪种隔离级别下可以避免。

一、脏读(Dirty Read)

现象描述:

事务 A 执行 UPDATE users SET balance = 200 WHERE id = 1;,但还没 COMMIT

事务 B 此时去查 SELECT balance FROM users WHERE id = 1;,读到了 200。

然后事务 A 发现不对劲,执行了 ROLLBACK,那行数据恢复成原来的值(比如 100)。

事务 B 手里拿着的 200 就是一笔"脏数据"——它从未真正存在于数据库中。这就是脏读。

为什么危险:

  • 事务 B 基于这个错误的值做了后续操作(比如扣款、计费),结果引用了一个从未提交的中间状态。
  • 银行、支付、库存这类场景,脏读是不能接受的。

哪个隔离级别能避免?

READ COMMITTED(读已提交) 及以上。实现方式:

  • 在该级别下,事务只能读到已提交的数据。
  • 具体实现上,PostgreSQL 直接用快照(Snapshot)实现,MySQL InnoDB 用 MVCC 的 Read View 来判断——只有已提交事务的版本才可见。

二、不可重复读(Non-repeatable Read)

现象描述:

事务 A 开始后做了两次相同的查询:

-- 第一次读
SELECT balance FROM users WHERE id = 1;   -- 返回 100

-- 此时事务 B 更新了这条记录
UPDATE users SET balance = 150 WHERE id = 1;  -- 并 COMMIT

-- 事务 A 第二次读同一行
SELECT balance FROM users WHERE id = 1;   -- 返回 150,和第一次不一样!

简单说就是:同一事务内,同一条记录,两次读取结果不一致。

与脏读的区别:

  • 脏读是读到了未提交的数据。
  • 不可重复读是读到了其他事务已提交的修改——关键在于数据是合法的、已提交的,只是事务 A 觉得"我刚才看到不是这个数啊"。

哪个隔离级别能避免?

REPEATABLE READ(可重复读) 及以上。

实现方式:

  • MVCC 快照读:事务首次读取时建立一个快照,之后所有读取都基于这个快照,其他事务的提交对其不可见。
  • PostgreSQL 的 REPEATABLE READ 就是靠快照实现的,同一事务内所有查询看到的都是事务开始时的数据。
  • MySQL InnoDB 也是靠 MVCC 实现,不过它把 REPEATABLE READ 作为默认级别。

三、幻读(Phantom Read)

现象描述:

事务 A 按范围查询了一次:

SELECT * FROM orders WHERE amount > 100;   -- 返回 3 行:id=1,2,3

-- 此时事务 B 插入了一笔新订单
INSERT INTO orders (id, amount) VALUES (4, 200);  -- 并 COMMIT

-- 事务 A 再次执行同样的范围查询
SELECT * FROM orders WHERE amount > 100;   -- 返回 4 行!多出了一行 id=4

"幻"就幻在这条新出现的行就像幽灵一样凭空冒出来了。

与不可重复读的区别(很多人混淆):

不可重复读 幻读
对象 已有行的值被修改/删除 新插入的行
场景 同一条数据两次取值不同 同一次查询多出行
感受 "值变了" "多了一条记录"

哪个隔离级别能避免?

SERIALIZABLE(可串行化)

实现方式:

  • 强制事务串行执行(实际是通过锁或冲突检测实现),从根本上杜绝任何并发冲突。
  • PostgreSQL 的 SERIALIZABLE 使用 SSI(Serializable Snapshot Isolation,可串行化快照隔离)技术,检测读写冲突后直接 abort 其中一个事务。
  • 代价就是并发性能大幅下降。

补充:MySQL InnoDB 的特殊情况

MySQL InnoDB 的 REPEATABLE READ 虽然没有 SERIALIZABLE 那么严格,但通过 间隙锁(Gap Lock) + Next-Key Lock 也能阻止幻读:

-- 事务 A
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
-- InnoDB 不光锁了现有行,还在索引间隙加了锁,阻止其他事务插入新行
-- 所以事务 B 的 INSERT 会被阻塞

因此在实际的 MySQL 环境中,REPEATABLE READ 就已经做到了避免幻读。这和 SQL 标准定义不完全一致——MySQL 文档自己也提到了这一点,所以大家在面试时要注意区分"SQL 标准定义"和"MySQL 实际行为"。


汇总表

隔离级别 脏读 不可重复读 幻读(SQL 标准)
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 避免 可能 可能
REPEATABLE READ 避免 避免 可能
SERIALIZABLE 避免 避免 避免
级别越高 一致性越强 并发性能越低

实际应用中大部分数据库的默认级别是 READ COMMITTED(PostgreSQL、Oracle、SQL Server)或 REPEATABLE READ(MySQL),选哪个看你业务对一致性和吞吐量的权衡。

第六问

为什么推荐使用自增主键?使用UUID作为主键的优缺点是什么?

为什么推荐自增主键

核心原因:性能优势

1. B+ 树写入特性

InnoDB使用聚簇索引(Clustered Index),数据按主键顺序物理存储。自增主键单调递增,新数据直接追加到 B+ 树末尾,不需要频繁的页分裂(Page Split)和节点重平衡(Rebalance)。

2. 空间利用率高

UUID(通用唯一识别码)主键比 BIGINT(8 字节整数)多占 3-4 倍存储空间。每张二级索引都会包含主键值,数据量上去后差距明显——16 字节的 UUID vs 8 字节的 BIGINT vs 4 字节的 INT。

3. 缓存友好

自增主键写入紧凑连续,磁盘和内存的缓存命中率更高。UUID 随机写入导致缓存频繁刷出刷入,LRU(最近最少使用)算法效率下降。

4. 简单可靠

不需要额外的生成逻辑,数据库内置机制保证唯一性和递增性。


UUID 主键的优缺点

缺点(通常不推荐)

问题 说明
随机写入 UUID 无序,写入时频繁触发页分裂,插入性能下降 50%-80%
存储膨胀 16 字节 vs BIGINT 8 字节;二级索引全部放大
索引碎片 随机写入导致 B+ 树(B+ Tree)碎片化严重,查询扫描范围变大
WHERE 效率 范围查询(BETWEEN、分页)无法利用物理连续性

优点(适合场景)

  • 分布式系统:多个节点独立生成 ID 无需中心协调,避免自增主键在分布式下的冲突问题
  • 数据合并:多个分库的数据合并到中心库时,UUID 天然不会冲突
  • 安全考虑:自增主键暴露业务规模(注册用户数、订单量等),UUID 不可预测
  • 离线写入:离线端生成 ID,联网后同步不会冲突

实用建议

  • 默认选自增主键(BIGINT UNSIGNED),绝大部分单体应用和微服务单库场景都足够
  • 需要 UUID 时,用 UUID 的变体
    • MySQL 8.0+:UUID_TO_BIN(UUID(), TRUE) 生成时序 UUID,对 B+ 树友好
    • 雪花算法(Snowflake ID):分布式友好、64 位数字、时序有序
    • 或者用 BIGINT UNSIGNED + 号段模式(Segment Mode)替代 UUID

总结:除非分布式场景或安全考虑是硬需求,否则自增主键在性能、存储、维护复杂度上都完胜 UUID。

第七问

什么是覆盖索引?它的优点是什么?

覆盖索引(Covering Index)

指一个索引包含了查询所需要的所有列,查询执行时只需要扫描索引本身就能获取全部结果,不需要回表到聚簇索引(主键索引)查找完整行记录。

举个例子:

-- 表
CREATE TABLE user (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  age INT,
  email VARCHAR(100)
);

-- 复合索引
CREATE INDEX idx_name_age ON user(name, age);

-- 这个查询的所有字段 (name, age) 都在索引中
SELECT name, age FROM user WHERE name = '张三';

MySQL 在 idx_name_age 索引的 B+ 树上遍历到 '张三' 后,直接拿到 nameage 返回,不再根据 id 回表去主键索引查其他字段。EXPLAINExtra 列会显示 Using index

优点

  1. 减少磁盘 I/O -- 索引体积通常远小于表数据(B+ 树只存索引列和主键),扫描索引比扫描整表需要的磁盘页更少
  2. 避免回表 -- 省去了从二级索引到聚簇索引的二次 B+ 树查找,这是最重要的性能提升点
  3. 减少缓冲池占用 -- 同样大小的内存可以缓存更多索引页,数据页访问次数下降
  4. 利用索引排序 -- 索引本身有序,ORDER BYGROUP BY 可以直接使用索引排序,避免文件排序(filesort
  5. 并发性能更好 -- 索引页更紧凑,减少了锁冲突和 I/O 竞争

需要注意的缺点:

  • 覆盖索引的列越多,索引体积越大,写入时的维护成本也越高
  • 不能把大字段(TEXT、BLOB 或超长的 VARCHAR)包含进来,否则索引臃肿反而拖慢查询
  • 覆盖索引是为了特定查询优化的,不是越多越好,需要结合业务查询模式来设计

第八问

请解释“回表”的概念。

回表是 InnoDB 存储引擎中,在使用**非聚簇索引(二级索引)**查询时的一种额外操作。

为什么会有回表

InnoDB 表的数据和主键索引是存储在一起的(聚簇索引),叶子节点存的是完整行数据。而二级索引的叶子节点存的是索引列的值 + 主键值,不是完整行。

当用二级索引查数据时:首先,在二级索引 B+ 树中定位到符合条件的叶子节点,拿到主键值;然后,拿着主键值到聚簇索引中再查一次,获取完整行数据。这第 2 步就是回表

举例说明

CREATE TABLE user (
  id   INT PRIMARY KEY,
  name VARCHAR(50),
  age  INT,
  KEY idx_age (age)
);

SELECT * FROM user WHERE age = 25;

执行流程:

  1. idx_age 二级索引,找到 age=25 的叶子节点,拿到 id=10
  2. 走聚簇索引(主键),找到 id=10 的完整行
  3. 返回结果

第 2 步就是回表。一次回表 = 一次随机 I/O。

如何避免回表

  • 覆盖索引:查询的列全部包含在二级索引中,就不需要回表
-- 不需要回表,因为 age 和 id 都在 idx_age 里
SELECT id, age FROM user WHERE age = 25;
  • 索引下推(ICP):MySQL 5.6+ 的特性,在二级索引扫描时提前过滤,减少回表次数

回表不是 bug,是 InnoDB 的存储结构决定的。优化的方向是:要么建覆盖索引,要么让回表次数尽量少。

Read more

Linux 运维文件写入操作

Linux 运维文件写入操作

面向 Linux 运维/SRE 的文件写入操作参考手册。既讲每条命令怎么用,也讲它背后的机制(为什么这么行为),配真实输出示例与边界情况,最后给出常见陷阱速查表与标准动作模板。 目录 1. 总览:运维"写文件"的全景 2. Shell 重定向:机制与基础符号 3. 重定向顺序的坑:为什么 2>&1 > f 不行 4. 文件描述符与 exec:持久打开、交换、关闭 5. 防止误覆盖:noclobber 与 >| 6. Here 文档与 Here 字符串 7. tee:

By Admin
Linux 定时任务(附学习指南)

Linux 定时任务(附学习指南)

Linux 系统的定时任务(Scheduled Tasks)是运维和开发中最常用的自动化手段之一。系统中最核心的两套定时任务工具是 cron(定时器) 和 at(一次性任务队列)。下面从原理到实战逐层展开。 一、cron:周期性定时任务 1. cron 是什么 cron 是一个守护进程(daemon),负责在约定的时间点自动执行用户定义的任务。它对应的服务名在大多数发行版中是 crond,相关的命令和文件包括: 命令/文件 作用 crond cron 守护进程 crontab 用户管理自己的定时任务 /etc/crontab 系统级定时任务(注意有用户字段,格式不同) /etc/cron.d/ 系统级定时任务的扩展目录(每个文件类似 /etc/crontab) /etc/cron.hourly/、daily/、weekly/

By Admin
自定义你的DSH

自定义你的DSH

DSH 界面主题定制插件 给 DSH 换一套界面主题 把设计 Token 变成所见即所得的「DIY 主题」设置面板。配色、字体、背景、阴影、圆角,改完立即生效,不用写一行代码。 GitHub 仓库 更多内容 DeepSeek Harness(DSH)是 DeepSeek 推出的 AI 编程助手框架。通过 dsh web 启动浏览器界面,就能和 AI 对话、读写文件、执行命令、规划任务。功能强大,但外观一直比较朴素,默认只有浅色、深色、跟随系统三档。 我写了一个插件 dsh-ui-customizer,把界面定制做成了所见即所得的「主题设置面板」:进入 设置

By Admin