<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>SQL on 张天赐的小世界</title>
    <link>https://www.zhangtianci.cn/tags/sql/</link>
    <description>Recent content in SQL on 张天赐的小世界</description>
    <generator>Hugo -- 0.151.0</generator>
    <language>zh-cn</language>
    <copyright>本博客的作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可</copyright>
    <lastBuildDate>Wed, 08 Oct 2025 23:18:00 +0800</lastBuildDate>
    <atom:link href="https://www.zhangtianci.cn/tags/sql/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>SQL 语句执行缓慢原因分析</title>
      <link>https://www.zhangtianci.cn/posts/sql-%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E7%BC%93%E6%85%A2%E5%8E%9F%E5%9B%A0%E5%88%86%E6%9E%90/</link>
      <pubDate>Sun, 05 Sep 2021 16:17:00 +0800</pubDate>
      <guid>https://www.zhangtianci.cn/posts/sql-%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E7%BC%93%E6%85%A2%E5%8E%9F%E5%9B%A0%E5%88%86%E6%9E%90/</guid>
      <description>&lt;h1 id=&#34;概述&#34;&gt;概述&lt;/h1&gt;
&lt;p&gt;SQL 查询性能优化是软件开发的核心技能之一。本文系统性地分析了 SQL 执行缓慢的各种原因，并提供了相应的诊断方法和解决方案。&lt;/p&gt;
&lt;h1 id=&#34;分类讨论&#34;&gt;分类讨论&lt;/h1&gt;
&lt;p&gt;SQL 执行缓慢可分为两种基本情况：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="概述">概述</h1>
<p>SQL 查询性能优化是软件开发的核心技能之一。本文系统性地分析了 SQL 执行缓慢的各种原因，并提供了相应的诊断方法和解决方案。</p>
<h1 id="分类讨论">分类讨论</h1>
<p>SQL 执行缓慢可分为两种基本情况：</p>
<ol>
<li>偶发性缓慢：大多数情况正常，偶尔出现性能问题</li>
<li>持续性缓慢：在数据量不变的情况下，一直执行缓慢</li>
</ol>
<p>针对这两种情况，我们来分析下可能是哪些原因导致的。</p>
<h1 id="针对偶尔很慢的情况">针对偶尔很慢的情况</h1>
<p>一条 SQL 大多数情况正常，偶尔才能出现很慢的情况，我觉得这条 SQL 语句的书写本身是没什么问题，而是其他原因导致。</p>
<h2 id="数据库在刷新脏页">数据库在刷新脏页</h2>
<p>当我们要往数据库插入一条数据、或者要更新一条数据的时候，我们知道数据库会在<strong>缓冲池</strong>中把对应字段的数据更新，但是更新之后，这些更新的字段并不会马上同步持久化到<strong>磁盘</strong>中去，而是把这些更新的记录写入到 <code>redo log</code> 中去。等到空闲的时候，在通过 <code>redo log</code> 里的记录把最新的数据同步到<strong>磁盘</strong>中去。</p>
<p>不过，<code>redo log</code> 里的容量是有限的。如果数据库一直很忙，更新又很频繁，这个时候 <code>redo log</code> 很快就会被写满，就没办法等到空闲的时候再把数据同步到磁盘，只能暂停其他操作，全身心来把数据同步到磁盘中去。而这个时候，<strong>就会导致我们平时正常的 SQL 语句突然执行的很慢</strong>。</p>
<h3 id="机制说明">机制说明</h3>
<ul>
<li>数据库使用缓冲池（Buffer Pool）在内存中缓存数据页</li>
<li>数据修改先在内存中完成，异步写入磁盘（Write-Ahead Logging）</li>
<li><code>Redo Log</code> 确保事务持久性，但容量有限</li>
</ul>
<h3 id="性能影响场景">性能影响场景</h3>
<ul>
<li><code>Redo Log</code> 写满时，必须强制刷脏页到磁盘</li>
<li>缓冲池空间不足，需要淘汰脏页</li>
<li>数据库正常关闭或检查点（Checkpoint）触发</li>
</ul>
<h3 id="监控与诊断">监控与诊断</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 查看InnoDB状态（包含缓冲池信息）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">ENGINE</span><span class="w"> </span><span class="n">INNODB</span><span class="w"> </span><span class="n">STATUS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 监控脏页比例
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Innodb_buffer_pool_pages_dirty&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="锁竞争问题">锁竞争问题</h2>
<p>这个就比较容易想到了。我们要执行的这条语句，刚好这条语句涉及到的表，别人在用，并且加锁。或者表没有加锁，但要使用到的某个一行被加锁。<strong>我们拿不到锁，只能慢慢等待别人释放锁</strong>。</p>
<p>如果要判断是否真的在等待锁，我们可以用 <code>show processlist</code> 这个命令来查看当前的状态。</p>
<h3 id="锁类型分析">锁类型分析</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>锁类型</strong></th>
          <th style="text-align: center"><strong>范围</strong></th>
          <th style="text-align: center"><strong>影响</strong></th>
          <th style="text-align: center"><strong>诊断方法</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">表级锁</td>
          <td style="text-align: center">整个表</td>
          <td style="text-align: center">高并发下严重影响性能</td>
          <td style="text-align: center"><code>SHOW PROCESSLIST</code></td>
      </tr>
      <tr>
          <td style="text-align: center">行级锁</td>
          <td style="text-align: center">单行记录</td>
          <td style="text-align: center">影响特定数据操作</td>
          <td style="text-align: center"><code>SHOW ENGINE INNODB STATUS</code></td>
      </tr>
      <tr>
          <td style="text-align: center">元数据锁</td>
          <td style="text-align: center">表结构变更</td>
          <td style="text-align: center">DDL 操作阻塞查询</td>
          <td style="text-align: center"><code>performance_schema.metadata_locks</code></td>
      </tr>
      <tr>
          <td style="text-align: center">间隙锁</td>
          <td style="text-align: center">索引范围</td>
          <td style="text-align: center">防止幻读，可能过度锁定</td>
          <td style="text-align: center">分析事务隔离级别</td>
      </tr>
  </tbody>
</table>
<h3 id="监控与诊断-1">监控与诊断</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 查看当前连接和锁状态
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">PROCESSLIST</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 查看InnoDB锁信息（MySQL 5.7+）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">data_locks</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">data_lock_waits</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 查看等待锁的线程
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">innodb_lock_waits</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="其他偶发因素">其他偶发因素</h2>
<p>还有一些其他原因和数据库内部机制，以及所在的的网络、物理机有关，本文不做详细讨论。</p>
<h3 id="系统资源瓶颈">系统资源瓶颈</h3>
<ul>
<li>CPU 瞬时峰值</li>
<li>内存交换 (SWAP) 发生</li>
<li>磁盘 I/O 瓶颈</li>
<li>网络波动</li>
</ul>
<h3 id="数据库内部机制">数据库内部机制</h3>
<ul>
<li>自适应哈希索引 (Adaptive Hash Index) 重建</li>
<li>变更缓冲区 (Change Buffer) 合并</li>
<li>统计信息自动更新</li>
</ul>
<h1 id="针对一直都这么慢的情况">针对一直都这么慢的情况</h1>
<p>下来我们来访分析下第二种情况，我觉得第二种情况的分析才是最重要的。</p>
<p>如果在数据量一样大的情况下，这条 SQL 语句每次都执行的这么慢，那就就要好好考虑下你的 SQL 书写了，下面我们来分析下哪些原因会导致我们的 SQL 语句执行的很不理想。</p>
<p>我们先来假设我们有一个表，表里有下面两个字段,分别是主键 id，和两个普通字段 c 和 d。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">mysql</span><span class="o">&gt;</span><span class="w"> </span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="o">`</span><span class="n">t</span><span class="o">`</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="k">c</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="n">d</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">11</span><span class="p">)</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">ENGINE</span><span class="o">=</span><span class="n">InnoDB</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="没用到索引">没用到索引</h2>
<p>没有用上索引，我觉得这个原因是很多人都能想到的，例如你要查询这条语句</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">100000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h3 id="字段没有索引">字段没有索引</h3>
<p>刚好你的 c 字段上没有索引，那么抱歉，只能走全表扫描了，你就体验不会索引带来的乐趣了，所以，这回导致这条查询语句很慢。</p>
<p>解决方案：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 分析查询模式后添加索引
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_c</span><span class="w"> </span><span class="p">(</span><span class="k">c</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_c_d</span><span class="w"> </span><span class="p">(</span><span class="k">c</span><span class="p">,</span><span class="w"> </span><span class="n">d</span><span class="p">);</span><span class="w"> </span><span class="c1">-- 复合索引
</span></span></span></code></pre></div><h3 id="没有用索引">没有用索引</h3>
<p>好吧，这个时候你给 c 这个字段加上了索引，然后又查询了一条语句</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>我想问这样在查询的时候会用索引查询吗？</p>
<p>答是不会，如果我们在字段的左边做了运算，那么很抱歉，在查询的时候，就不会用上索引了，所以要注意这种<strong>字段上有索引，但由于自己的疏忽，导致系统没有使用索引</strong>的情况。</p>
<p>正确的查询应该如下</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>有人可能会说，右边有运算就能用上索引？难道数据库就不会自动帮我们优化一下，自动把 c - 1=1000 自动转换为 c = 1000+1。</p>
<p>不好意思，确实不会帮你，所以，你要注意了。</p>
<p>如果我们在查询的时候，对字段进行了函数操作，也是会导致没有用上索引的，例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="n">pow</span><span class="p">(</span><span class="k">c</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>这里我只是做一个例子，实际上可能并没有 <code>pow(c)</code> 这个函数。其实这个和上面在左边做运算也是很类似的。</p>
<p>所以呢，一条语句执行都很慢的时候，可能是该语句没有用上索引了，不过具体是啥原因导致没有用上索引的呢，你就要会分析了，我上面列举的三个原因，应该是出现的比较多的。</p>
<h3 id="常见索引失效情况">常见索引失效情况</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>失效模式</strong></th>
          <th style="text-align: center"><strong>示例</strong></th>
          <th style="text-align: center"><strong>解决方案</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">左侧运算</td>
          <td style="text-align: center"><code>WHERE c - 1 = 1000</code></td>
          <td style="text-align: center">重写为 <code>WHERE c = 1000 + 1</code></td>
      </tr>
      <tr>
          <td style="text-align: center">函数操作</td>
          <td style="text-align: center"><code>WHERE DATE(create_time) = '2023-01-01'</code></td>
          <td style="text-align: center">使用范围查询 <code>WHERE create_time &gt;= '2023-01-01' AND create_time &lt; '2023-01-02'</code></td>
      </tr>
      <tr>
          <td style="text-align: center">隐式类型转换</td>
          <td style="text-align: center"><code>WHERE string_col = 123</code></td>
          <td style="text-align: center">保持类型一致 <code>WHERE string_col = '123'</code></td>
      </tr>
      <tr>
          <td style="text-align: center">OR 条件不当</td>
          <td style="text-align: center"><code>WHERE c = 100 OR d = 200</code></td>
          <td style="text-align: center">使用 UNION 或分别索引</td>
      </tr>
      <tr>
          <td style="text-align: center">模糊查询前缀</td>
          <td style="text-align: center"><code>WHERE name LIKE '%abc'</code></td>
          <td style="text-align: center">避免前导通配符</td>
      </tr>
  </tbody>
</table>
<h3 id="索引设计原则">索引设计原则</h3>
<ul>
<li>高选择性字段优先建索引</li>
<li>考虑复合索引的字段顺序</li>
<li>避免过度索引（写操作开销）</li>
<li>覆盖索引减少回表</li>
</ul>
<h2 id="数据库选错索引">数据库选错索引</h2>
<p>我们在进行查询操作的时候，例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-mysql" data-lang="mysql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">c</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="n">c</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">100000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>我们知道，主键索引和非主键索引是有区别的，主键索引存放的值是<strong>整行字段的数据</strong>，而非主键索引上存放的值不是整行字段的数据，而且存放<strong>主键字段的值</strong>。</p>
<p>也就是说，我们如果走 c 这个字段的索引的话，最后会查询到对应主键的值，然后，再根据主键的值走主键索引，查询到整行数据返回。</p>
<p>就算你在 c 字段上有索引，系统也并不一定会走 c 这个字段上的索引，而是有可能会直接扫描扫描全表，找出所有符合 100 &lt; c and c &lt; 100000 的数据。</p>
<p>系统在执行这条语句的时候，会进行预测：究竟是走 c 索引扫描的行数少，还是直接扫描全表扫描的行数少呢？显然，扫描行数越少当然越好了，因为扫描行数越少，意味着 I/O 操作的次数越少。</p>
<p>如果是扫描全表的话，那么扫描的次数就是这个表的总行数了，假设为 n；而如果走索引 c 的话，我们通过索引 c 找到主键之后，还得再通过主键索引来找我们整行的数据，也就是说，需要走两次索引。而且，我们也不知道符合 100 c &lt; and c &lt; 10000 这个条件的数据有多少行，万一这个表是全部数据都符合呢？这个时候意味着，走 c 索引不仅扫描的行数是 n，同时还得每行数据走两次索引。</p>
<p><strong>所以系统是有可能走全表扫描而不走索引的。那系统是怎么判断呢？</strong></p>
<h3 id="索引判断原则">索引判断原则</h3>
<p>判断来源于系统的预测，也就是说，如果要走 c 字段索引的话，系统会预测走 c 字段索引大概需要扫描多少行。如果预测到要扫描的行数很多，它可能就不走索引而直接扫描全表了。</p>
<p>系统是通过<strong>索引的区分度</strong>来判断的，一个索引上不同的值越多，意味着出现相同数值的索引越少，意味着索引的区分度越高。我们也把区分度称之为<strong>基数</strong>，即区分度越高，基数越大。所以呢，基数越大，意味着符合 100 &lt; c and c &lt; 10000 这个条件的行数越少。</p>
<p>所以呢，一个索引的基数越大，意味着走索引查询越有优势。</p>
<p><strong>那么问题来了，怎么知道这个索引的基数呢？</strong></p>
<p>系统当然是不会遍历全部来获得一个索引的基数的，代价太大了，索引系统是通过遍历部分数据，也就是通过<strong>采样</strong>的方式，来预测索引的基数的。</p>
<p><strong>扯了这么多，重点的来了</strong>，居然是采样，那就有可能出现<strong>失误</strong>的情况，也就是说，c 这个索引的基数实际上是很大的，但是采样的时候，却很不幸，把这个索引的基数预测成很小。例如你采样的那一部分数据刚好基数很小，然后就误以为索引的基数很小。<strong>然后就呵呵，系统就不走 c 索引了，直接走全部扫描</strong>。</p>
<p>所以呢，说了这么多，得出结论：<strong>由于统计的失误，导致系统没有走索引，而是走了全表扫描</strong>，而这，也是导致我们 SQL 语句执行的很慢的原因。</p>
<blockquote>
<p>这里我声明一下，系统判断是否走索引，扫描行数的预测其实只是原因之一，这条查询语句是否需要使用使用临时表、是否需要排序等也是会影响系统的选择的。</p></blockquote>
<p>影响因素：</p>
<ul>
<li>索引选择性</li>
<li>预计需要回表的次数</li>
<li>临时表、排序开销</li>
<li>历史执行统计（MySQL 8.0+）</li>
</ul>
<h3 id="解决方案">解决方案</h3>
<p>不过呢，我们有时候也可以通过强制走索引的方式来查询，例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">FORCE</span><span class="w"> </span><span class="k">INDEX</span><span class="p">(</span><span class="n">idx_c</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">100000</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>我们也可以通过</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>来查询索引的基数和实际是否符合，如果和实际很不符合的话，我们可以重新来统计索引的基数，可以用这条命令</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ANALYZE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>来重新统计分析。</p>
<p><strong>既然会预测错索引的基数，这也意味着，当我们的查询语句有多个索引的时候，系统有可能也会选错索引</strong>，这也可能是 SQL 执行的很慢的一个原因。</p>
<h1 id="系统化诊断流程">系统化诊断流程</h1>
<p>上述问题应该怎么发现呢？我列举了一些常用的手段。</p>
<h2 id="性能分析工具">性能分析工具</h2>
<h3 id="explain">EXPLAIN</h3>
<p>通过查看执行计划，提前避免问题。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="n">FORMAT</span><span class="o">=</span><span class="n">JSON</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>关键指标关注：</p>
<ul>
<li><code>type</code>：访问类型（const, ref, range, index, ALL）</li>
<li><code>key</code>：实际使用的索引</li>
<li><code>rows</code>：预估扫描行数</li>
<li><code>Extra</code>：额外信息（Using where, Using temporary, Using filesort）</li>
</ul>
<h3 id="性能监控performance-schema">性能监控（Performance Schema）</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 开启语句监控
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">setup_consumers</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">ENABLED</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;YES&#39;</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">NAME</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;events_statements%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 查看慢查询统计
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">events_statements_summary_by_digest</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">SUM_TIMER_WAIT</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h3 id="慢查询日志分析">慢查询日志分析</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># my.cnf 配置</span>
</span></span><span class="line"><span class="cl"><span class="na">slow_query_log</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">slow_query_log_file</span> <span class="o">=</span> <span class="s">/var/log/mysql/slow.log</span>
</span></span><span class="line"><span class="cl"><span class="na">long_query_time</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">log_queries_not_using_indexes</span> <span class="o">=</span> <span class="s">1</span>
</span></span></code></pre></div><h2 id="高级优化策略">高级优化策略</h2>
<h3 id="架构层面优化">架构层面优化</h3>
<ul>
<li>读写分离</li>
<li>分库分表</li>
<li>缓存策略（Redis, Memcached）</li>
<li>数据归档和历史表</li>
</ul>
<h3 id="数据库参数调优">数据库参数调优</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># InnoDB缓冲池（通常设为物理内存的70-80%）</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_buffer_pool_size</span> <span class="o">=</span> <span class="s">16G</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 日志文件大小</span>
</span></span><span class="line"><span class="cl"><span class="na">innodb_log_file_size</span> <span class="o">=</span> <span class="s">2G</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 并发连接控制</span>
</span></span><span class="line"><span class="cl"><span class="na">max_connections</span> <span class="o">=</span> <span class="s">500</span>
</span></span><span class="line"><span class="cl"><span class="na">thread_cache_size</span> <span class="o">=</span> <span class="s">50</span>
</span></span></code></pre></div><h3 id="应用层优化">应用层优化</h3>
<ul>
<li>连接池配置</li>
<li>批量操作减少网络往返</li>
<li>预处理语句避免重复解析</li>
<li>适当的数据缓存</li>
</ul>
<h1 id="总结">总结</h1>
<p>SQL 性能优化是一个系统性的工程，需要从多个维度进行分析和解决。本文提供的分析框架和优化策略覆盖了从基础到高级的各个层面，可以作为日常性能优化的参考指南。</p>
<p><strong>核心要点</strong>：</p>
<ul>
<li>建立系统化的诊断流程</li>
<li>索引是最高效的优化手段</li>
<li>统计信息的准确性至关重要</li>
<li>监控和预防优于事后补救</li>
<li>优化需要综合考虑成本和收益</li>
</ul>
<p>建议定期进行数据库健康检查，建立性能基线，以便快速识别和解决性能问题。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
