<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>EXPLAIN on 姚玉亮的网络日志</title><link>https://yusanwen-code.github.io/tags/explain/</link><description>Recent content in EXPLAIN on 姚玉亮的网络日志</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Wed, 26 Nov 2025 10:30:00 +0800</lastBuildDate><atom:link href="https://yusanwen-code.github.io/tags/explain/index.xml" rel="self" type="application/rss+xml"/><item><title>MySQL 复杂查询优化：从 EXPLAIN 到索引重构</title><link>https://yusanwen-code.github.io/posts/post-53/</link><pubDate>Wed, 26 Nov 2025 10:30:00 +0800</pubDate><guid>https://yusanwen-code.github.io/posts/post-53/</guid><description>&lt;img src="https://yusanwen-code.github.io/images/post-53-cover.jpg" alt="Featured image of post MySQL 复杂查询优化：从 EXPLAIN 到索引重构" /&gt;&lt;h2 id="一个-5-秒的检索接口"&gt;一个 5 秒的检索接口&#10;&lt;/h2&gt;&lt;p&gt;数据治理服务里有个数据资产检索页，支持按租户、数据集类型、更新时间区间、关键词多条件筛选。底表是几百万行的 &lt;code&gt;dataset&lt;/code&gt; 表，还要 LEFT JOIN &lt;code&gt;owner&lt;/code&gt; 表拿负责人名字。上线初期一个查询要 3 到 5 秒，ELK 里慢查询日志刷屏。我负责这次优化，目标定得比较克制：不改业务语义，把 P95 压到 500ms 以内。&lt;/p&gt;&#10;&lt;h2 id="explain-先说话"&gt;EXPLAIN 先说话&#10;&lt;/h2&gt;&lt;p&gt;先跑 EXPLAIN 看执行计划，问题直接摆在脸上：&lt;code&gt;type=ALL&lt;/code&gt; 的全表扫描，外加 &lt;code&gt;Using filesort&lt;/code&gt;。几百万行先全扫一遍再排一次序，5 秒就是这么烧掉的。&lt;/p&gt;&#10;&lt;p&gt;我的思路分四块：为高频过滤条件建联合索引，让 WHERE 和 ORDER BY 走同一棵 B+Tree；&lt;code&gt;LIKE '%关键词%'&lt;/code&gt; 这种模糊匹配 MySQL 帮不上忙，卸载给 ES；JOIN 只留必要的，被驱动表走主键；深翻页改成游标分页。&lt;/p&gt;&#10;&lt;p&gt;原始 GORM 查询长这样：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 2&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 3&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 4&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 5&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 6&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 7&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 8&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 9&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;10&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;11&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;SearchDatasets&lt;/span&gt;(db&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;gorm.DB,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;tenantID&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;uint&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;typ&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;string&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;from,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;to&lt;span style="color:#6e7681"&gt; &lt;/span&gt;time.Time)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;([]Dataset,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;error&lt;/span&gt;)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;var&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;list&lt;span style="color:#6e7681"&gt; &lt;/span&gt;[]Dataset&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;db.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Table&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;dataset d&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Select&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;d.*, o.name as owner_name&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Joins&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;LEFT JOIN owner o ON o.id = d.owner_id&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Where&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;d.tenant_id = ? AND d.type = ? AND d.updated_at BETWEEN ? AND ?&amp;#34;&lt;/span&gt;,&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;tenantID,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;typ,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;from,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;to).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Order&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;d.updated_at DESC&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Limit&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;20&lt;/span&gt;).&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Find&lt;/span&gt;(&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;list).Error&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;list,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;EXPLAIN 结果（简化）：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;2&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;id table type key rows Extra&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1 d ALL NULL 820000 Using where; Using filesort&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1 o eq_ref PRIMARY 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;问题很清楚：dataset 表全表扫外加 filesort。加联合索引：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;2&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;ALTER&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;TABLE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;dataset&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;ADD&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INDEX&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;idx_tenant_type_updated&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(tenant_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;type&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;updated_at);&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;优化后让 GORM 走这个索引：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;2&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;3&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;4&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;5&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;6&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;7&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;db.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Table&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;dataset d FORCE INDEX (idx_tenant_type_updated)&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Select&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;d.id, d.title, d.type, d.updated_at, d.owner_id, o.name as owner_name&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Joins&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;LEFT JOIN owner o ON o.id = d.owner_id&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Where&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;d.tenant_id = ? AND d.type = ? AND d.updated_at &amp;gt;= ? AND d.updated_at &amp;lt; ?&amp;#34;&lt;/span&gt;,&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;tenantID,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;typ,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;from,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;to).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Order&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;d.updated_at DESC&amp;#34;&lt;/span&gt;).&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Limit&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;20&lt;/span&gt;).&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Find&lt;/span&gt;(&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;list).Error&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;这里 FORCE INDEX 是有意的：统计信息不准的时候优化器会选错索引，干脆指定，不让它猜。&lt;/p&gt;&#10;&lt;p&gt;EXPLAIN 变成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;2&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;id table type key rows Extra&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1 d range idx_tenant_type_updated 1200 Using index condition&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1 o eq_ref PRIMARY 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;rows 从 820000 降到 1200，filesort 也消失了。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="一条查询怎么从全表扫描变成索引定位" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://yusanwen-code.github.io/images/post-53-explain-index.svg"&gt;&lt;/p&gt;&#10;&lt;h2 id="六个坑"&gt;六个坑&#10;&lt;/h2&gt;&lt;p&gt;第一个要认的是最左前缀。联合索引 (tenant_id, type, updated_at) 必须 tenant_id 打头才用得上。如果业务里 type 会单独查而 tenant_id 不一定传，就得评估再建一个 (type, updated_at)，别指望一个索引通吃所有查询。&lt;/p&gt;&#10;&lt;p&gt;第二，范围列放最后。updated_at 用了 BETWEEN / &amp;gt;= 之后，排在它后面的索引列就没法再用于定位了。不过排序还能吃到这里，所以 ORDER BY 的方向要和索引一致。&lt;/p&gt;&#10;&lt;p&gt;第三，&lt;code&gt;LIKE '%xxx%'&lt;/code&gt; 走不了 B+Tree，这个没得商量。我们把标题、摘要同步到 ES，MySQL 只承担结构化过滤。异构索引听着重，其实就是让两个引擎各干各擅长的。&lt;/p&gt;&#10;&lt;p&gt;第四，别 &lt;code&gt;SELECT *&lt;/code&gt;。PDF 解析出来的大 content 字段单独放从表或对象存储，列表查询只取需要的列，回表开销小一大截。&lt;/p&gt;&#10;&lt;p&gt;第五，索引不是越多越好。dataset 写入频繁，每加一个索引，写放大就多一分。我们控制在 5 个以内，定期用 &lt;code&gt;pt-duplicate-key-checker&lt;/code&gt; 清理冗余。&lt;/p&gt;&#10;&lt;p&gt;第六是深翻页。&lt;code&gt;LIMIT 100000, 20&lt;/code&gt; 会老老实实扫完前 10 万行再扔掉，改成基于上一页最后一条的 &lt;code&gt;updated_at &amp;lt; ?&lt;/code&gt; 游标分页，翻得再深，成本也不涨。&lt;/p&gt;&#10;&lt;h2 id="后来"&gt;后来&#10;&lt;/h2&gt;&lt;p&gt;回头看，EXPLAIN 是基本功，重点盯 &lt;code&gt;type&lt;/code&gt;、&lt;code&gt;key&lt;/code&gt;、&lt;code&gt;rows&lt;/code&gt;、&lt;code&gt;Extra&lt;/code&gt; 四列；索引设计要贴着真实的 WHERE 和 ORDER BY 来，不是凭感觉给每个字段撒一遍。模糊搜索交给 ES，MySQL 只干它最擅长的结构化查询。这套组合在数据治理服务里稳定扛住了日常的数据资产检索。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;封面图：&lt;a class="link" href="https://www.flickr.com/photos/7400937@N07/5101688010" target="_blank" rel="noopener"&#10; &gt;David W. Siu / Flickr&lt;/a&gt; · CC BY 2.0&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>