<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>MySQL on 追风笔记</title><link>https://f91dba72.wangpeng.pages.dev/tags/mysql/</link><description>Recent content in MySQL on 追风笔记</description><generator>Hugo</generator><language>zh</language><lastBuildDate>Tue, 25 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://f91dba72.wangpeng.pages.dev/tags/mysql/index.xml" rel="self" type="application/rss+xml"/><item><title>MySQL 索引原理与最佳实践</title><link>https://f91dba72.wangpeng.pages.dev/tech/mysql/indexing/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/tech/mysql/indexing/</guid><description>&lt;p&gt;索引是 MySQL 查询性能的决定性因素。理解 InnoDB 的 B+Tree 索引结构，才能写出真正命中索引的 SQL。&lt;/p&gt;&#10;&lt;h2 id="btree-与聚簇索引"&gt;B+Tree 与聚簇索引&#10;&lt;/h2&gt;&#10;&lt;p&gt;InnoDB 使用 B+Tree 组织索引，其特点是非叶子节点只存索引键用于导航，所有数据都落在叶子节点，且叶子节点通过双向链表串联，非常适合范围扫描。&lt;/p&gt;&#10;&lt;p&gt;InnoDB 的主键索引是&lt;strong&gt;聚簇索引&lt;/strong&gt;，叶子节点直接存放整行数据；二级索引的叶子节点存放「索引键 + 主键值」。因此通过二级索引查询非索引列时，需要再用主键回表查聚簇索引。&lt;/p&gt;&#10;&lt;h2 id="回表与覆盖索引"&gt;回表与覆盖索引&#10;&lt;/h2&gt;&#10;&lt;p&gt;当查询的列都在二级索引中时，无需回表，称为&lt;strong&gt;覆盖索引&lt;/strong&gt;，性能更好：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-5f7f444b-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="3"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-5f7f444b-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- idx_user_age 为 (name, age) 的联合索引&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 仅取索引列，命中覆盖索引&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tom&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;若 &lt;code&gt;SELECT *&lt;/code&gt; 还需要 &lt;code&gt;phone&lt;/code&gt; 等额外列，则会触发回表。高频查询应优先设计为覆盖索引。&lt;/p&gt;&#10;&lt;h2 id="最左前缀原则"&gt;最左前缀原则&#10;&lt;/h2&gt;&#10;&lt;p&gt;联合索引 &lt;code&gt;(a, b, c)&lt;/code&gt; 能生效的条件是查询从最左列开始连续匹配：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;WHERE a = ?&lt;/code&gt; 可用&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;WHERE a = ? AND b = ?&lt;/code&gt; 可用&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;WHERE b = ?&lt;/code&gt; 无法使用该联合索引（断开了最左前缀）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="避免索引失效"&gt;避免索引失效&#10;&lt;/h2&gt;&#10;&lt;p&gt;常见导致索引失效的写法：&lt;/p&gt;</description></item><item><title>MySQL 事务与锁机制</title><link>https://f91dba72.wangpeng.pages.dev/tech/mysql/transaction-lock/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/tech/mysql/transaction-lock/</guid><description>&lt;p&gt;并发场景下，事务与锁共同保证了数据的一致性。InnoDB 通过 MVCC 与锁的协同，在性能与隔离性之间取得平衡。&lt;/p&gt;&#10;&lt;h2 id="acid-与隔离级别"&gt;ACID 与隔离级别&#10;&lt;/h2&gt;&#10;&lt;p&gt;事务的 ACID 由不同机制保障：原子性靠 undo log，持久性靠 redo log，隔离性靠 MVCC 与锁，一致性是最终目标。&lt;/p&gt;&#10;&lt;p&gt;SQL 标准定义四种隔离级别，InnoDB 默认 &lt;code&gt;REPEATABLE READ&lt;/code&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;READ UNCOMMITTED：可能脏读&lt;/li&gt;&#10;&lt;li&gt;READ COMMITTED：避免脏读，可能不可重复读&lt;/li&gt;&#10;&lt;li&gt;REPEATABLE READ：避免不可重复读（InnoDB 额外避免幻读）&lt;/li&gt;&#10;&lt;li&gt;SERIALIZABLE：完全串行，性能最低&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="mvcc-与日志"&gt;MVCC 与日志&#10;&lt;/h2&gt;&#10;&lt;p&gt;MVCC（多版本并发控制）让读不加锁、读写不阻塞。每行记录隐含 &lt;code&gt;trx_id&lt;/code&gt; 与 &lt;code&gt;roll_pointer&lt;/code&gt;，通过 undo log 构建历史版本，配合 ReadView 判断某版本对当前事务是否可见。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;undo log&lt;/strong&gt;：记录数据修改前的镜像，用于回滚与构建旧版本。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;redo log&lt;/strong&gt;：记录物理页修改，保证崩溃后已提交事务不丢失（WAL 机制）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="行锁间隙锁与-next-key-lock"&gt;行锁、间隙锁与 Next-Key Lock&#10;&lt;/h2&gt;&#10;&lt;p&gt;InnoDB 默认使用行锁，但「锁的是索引记录」而非行本身。在 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 下，为解决幻读引入：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;行锁（Record Lock）&lt;/strong&gt;：锁住具体索引记录。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;间隙锁（Gap Lock）&lt;/strong&gt;：锁住索引记录之间的间隙，阻止插入。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Next-Key Lock&lt;/strong&gt;：行锁 + 间隙锁，锁定左开右闭的区间。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-634a084d-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="2"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-634a084d-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 在 (10, 20] 区间加 Next-Key Lock，阻止其他事务插入 id=15&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BETWEEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="死锁成因与排查"&gt;死锁成因与排查&#10;&lt;/h2&gt;&#10;&lt;p&gt;死锁通常由两个事务以相反顺序获取锁引起。排查手段：&lt;/p&gt;</description></item><item><title>分库分表不是银弹</title><link>https://f91dba72.wangpeng.pages.dev/blog/mysql-sharding/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/mysql-sharding/</guid><description>&lt;p&gt;分库分表在很多团队是一种默认操作：数据量一大就分。但它换来的收益很窄，付出的代价很宽，而且代价大多在上线半年后才开始显现。&lt;/p&gt;&#10;&lt;h2 id="先问要不要分"&gt;先问要不要分&#10;&lt;/h2&gt;&#10;&lt;p&gt;分库分表真正解决的问题只有两个：单表数据量导致的索引效率下降，以及单库写入的 IOPS 瓶颈。除此之外的问题它都解决不了，甚至会恶化。&lt;/p&gt;&#10;&lt;p&gt;在决定分之前，这几件事的性价比更高：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;加索引或改查询&lt;/strong&gt;。慢查询里相当大一部分是索引缺失或者写法导致索引失效，跟数据量无关。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;冷热分离&lt;/strong&gt;。交易流水表里九成查询集中在最近三个月。把历史数据归档到历史库，主表立刻瘦身，成本远低于分表。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;读写分离&lt;/strong&gt;。读多写少的场景加从库就够了。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;升配&lt;/strong&gt;。听起来不体面，但一台高配实例能撑到的量往往超出预期，而且省下的是人力。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;我的经验阈值：单表超过两千万行且增长稳定、或者写入已经打满 IO，才考虑分表。仅仅因为&amp;quot;数据量看着挺大&amp;quot;就分，是给自己找活干。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="分片键决定生死"&gt;分片键决定生死&#10;&lt;/h2&gt;&#10;&lt;p&gt;分片键选错基本没法补救——除非停机重新迁移全量数据。&lt;/p&gt;&#10;&lt;p&gt;选择原则是&lt;strong&gt;让高频查询都能带上分片键&lt;/strong&gt;。交易流水表用账号分片，那么&amp;quot;查某账号最近流水&amp;quot;就是单分片查询，性能很好；但&amp;quot;查某商户当天全部流水&amp;quot;就变成了全分片扫描加内存聚合。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-35cd25d1-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="10"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-35cd25d1-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 单分片，走得很好&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_flow&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account_no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;6222...1234&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-06-01&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 跨全部分片，聚合在应用层做，分片多了就是灾难&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;merchant_no&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_flow&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-06-14&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;merchant_no&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;第二类需求不要靠分片库硬扛，应该走另一条路：同步一份到 OLAP 引擎或数仓。&lt;strong&gt;一份数据满足所有查询模式的想法本身就是问题的根源。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;另外一定要&lt;strong&gt;一次性把逻辑分片数定足&lt;/strong&gt;。我们定了 1024 个逻辑分片映射到 8 个物理库，扩容时只搬分片区间，不用重新哈希。如果直接按物理库取模，第一次扩容就要迁移全量数据。&lt;/p&gt;</description></item></channel></rss>