ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL索引设计实战:从B+树原理到慢查询优化

MySQL索引设计实战:从B+树原理到慢查询优化 1. 为什么索引设计决定了数据库的生死线先讲一个我真实经历过的场景。几年前接手过一个线上商城项目订单表已经有3000万行数据某天运营反馈后台导出报表特别慢一查慢查询日志一条按用户ID加下单时间查订单的SQL跑了8秒多。DBA团队一看执行计划全表扫描。用户表才几万人订单表几千万行全表扫一遍不慢才怪。这件事当时给了我一个非常深刻的教训索引不是数据库的附属品而是和数据模型、业务查询路径强耦合的设计产物。很多团队把索引当成“慢查询出现之后再加的东西”其实是本末倒置了。索引设计应该发生在建表阶段而不是出了问题再去补救。补救式的加索引往往意味着数据已经膨胀到难以低成本修改结构的地步而且ALTER TABLE在大表上加索引会锁表或者消耗大量IO线上业务受影响极大。索引设计的本质是在“写入性能”和“查询性能”之间做权衡。索引不是越多越好每增加一个索引插入、更新、删除时都要同步维护索引结构代价是实打实的。所以设计原则的核心可以浓缩成一句话用最少的索引覆盖最多的查询路径。这篇内容没有花架子我会把我在实际项目中验证过的索引设计方法、踩过的坑、排查手段全部梳理出来从原理到实操尽可能让它成为一份可以直接拿来用的参考手册比较适合后端开发、DBA、以及所有需要写SQL的人。2. 索引底层原理不了解B树设计原则全是空中楼阁2.1 B树到底做了什么索引设计原则背后站着的是B树这个数据结构。我们常说“加了索引查询快”但快的原因得说清楚。MySQL InnoDB引擎的索引底层是B树它有几个关键特性所有数据都存储在叶子节点非叶子节点只存键值和指针叶子节点之间通过链表串联并且数据是按键值有序排列的。这里有个很关键的细节因为叶子节点有序且用链表串起来所以范围查询BETWEEN、、非常高效找到起点之后顺着链表扫就行不需要反复从根节点遍历。这也解释了为什么ORDER BY在走索引的情况下不需要再额外的filesort因为索引本身就是有序的。再来看存储InnoDB是聚簇索引表。什么意思就是说表里的数据行物理上就是按照主键顺序存储在B树的叶子节点上的。主键索引的叶子节点直接存的是整行数据。而二级索引我们手动加的普通索引、联合索引的叶子节点存的是索引键值加主键值。这里引出了“回表”的概念。举个例子一张用户表主键是id我们给user_name建了普通索引。执行SELECT * FROM user WHERE user_name 张三时MySQL会先去user_name这个二级索引的B树里找到“张三”对应的主键值id 100然后再拿着100去主键索引的B树里找完整的行数据。这个过程就是回表。如果查询的字段恰好都在二级索引里就无需回表了这就是后面要讲的覆盖索引优化的底层依据。2.2 聚簇索引和二级索引的存储差异很多人以为“索引就是给字段加个索引然后查询就快了”实际情况要复杂得多。聚簇索引决定了表的物理存储顺序所以主键的选择本身就是索引设计的一部分。这也是为什么强烈推荐使用自增整数做主键而不是UUID。UUID无序且长插入时B树需要频繁做页分裂和重排写入性能会明显变差而且占用空间大导致每个二级索引的叶子节点都更大因为二级索引存了主键值。再补充一个底层视角B树的高度决定了查询需要的磁盘IO次数。假设一个B树的非叶子节点能存上千个键值三层的B树就能支撑上千万行数据。所以即使数据行数上了千万走主键查询也就三次磁盘IO的事。但如果索引不合理导致全表扫描那是完全不同的数量级。理解了这些你才能理解下面所有设计原则背后的合理性。索引设计不是说“给查询字段加索引就行”而是要清楚每一个索引背后的存储代价、查询代价、维护代价。3. 索引设计核心原则六个能直接落地的方法3.1 最左前缀原则联合索引的根基联合索引是实际开发中用得最多的多列索引它遵循最左前缀原则。也就是说创建一个联合索引(a, b, c)实际生效的组合是(a)、(a, b)、(a, b, c)。查询条件里没有a直接查b或者c在这个索引上是走不了的。这个原则背后是B树的排序逻辑先按第一列排序第一列相同再按第二列排以此类推。所以跳过了第一列后面的列的顺序信息就没有意义了。这里要重点说一下热搜词里那个典型的疑惑——“mysql where条件a and b应该怎么建索引”。这个问题的答案就是最左前缀原则。如果查询场景是等值查询WHERE a ? AND b ?建联合索引(a, b)就行。(a, b)和(b, a)的区别在于如果还需要单独查WHERE a ?那么(a, b)能覆盖两个场景如果还需要单独查WHERE b ?那么(a, b)无法覆盖需要额外单独给b建索引或者建(b, a)。实际操作中要把查询频率最高的字段放在最左边。高频组合查询里还有一个细节值得提等值查询和排序字段混在一起时排序字段应该放在联合索引的后面。比如高频查询是WHERE a ? ORDER BY b索引设计为(a, b)B树直接按a定位后b天然有序不需要额外的排序操作。3.2 区分度原则用数据算出来而不是拍脑袋区分度Cardinality是指字段中不重复值的比例。区分度越高索引的过滤效果越好。比如性别字段只有“男”“女”两种值区分度极低建了索引也没啥用因为扫一遍索引可能拿回一半的行MySQL优化器大概率会选择全表扫描。具体怎么算一条SQL就能搞定SELECT COUNT(DISTINCT column_name) / COUNT(*) FROM table_name;比例越接近1区分度越好。一般来说区分度低于20%的字段建索引的收益就比较有限了。但这也不是绝对的。区分度低的字段在“联合索引前面的位置”依然有用因为联合索引最左列在等值条件下会先做一次过滤缩减范围。比如一个订单表status字段只有3个值单独建索引没意义但如果查询模式是WHERE status ? AND create_time BETWEEN ? AND ?那么(status, create_time)联合索引反而是合理的先通过status剔除大部分行再用create_time精确扫描。这个思路往往被忽略但实际项目里非常常用。3.3 冗余索引要杜绝冗余索引是新手最喜欢犯的错误。最常见的情况是已经建了联合索引(a, b)然后又单独建了a的索引。因为(a, b)的最左前缀已经覆盖了WHERE a ?的查询单独再建a的索引属于完全冗余。每一次插入和更新都会多维护一棵B树纯亏。还有一种隐蔽的冗余(a, b)和(a, c)如果查询中b和c不会同时出现那么这两个索引不是严重冗余但如果有场景同时查b和c其实说明该考虑(a, b, c)或者(a, c, b)了。冗余索引排查的手段比较简单把数据库里所有索引列出来逐一去看哪些索引是已有索引的最左前缀直接删。3.4 覆盖索引优先把查询字段放进去覆盖索引指的是查询的字段全部包含在索引中不需要回表。这可能是性价比最高的优化手段。举个例子订单查询里高频场景是SELECT order_id, status FROM order WHERE user_id ?。如果只给user_id建索引每次查询都要回表拿status字段等于做了两次索引查找。但如果我们建(user_id, status)联合索引索引的叶子节点上已经包含了这两个值查询结果可以直接从索引返回省掉回表。这里有个常见的误解覆盖索引不是说建索引就要把所有字段塞进去。索引列的增多会降低写入性能和增加存储空间所以要优先覆盖高频查询中的字段而不是茫茫多全部塞进去。把一张表的所有字段建进索引不可取存储成本太高更新时每个索引都要跟着动。3.5 合理设计主键索引主键索引是每个表默认有的聚簇索引。推荐使用自增整数主键。自增主键插入时是顺序写B树叶子节点直接在尾部追加页分裂极少发生。而UUID主键是随机写动不动就要重排节点数据量大了之后写入性能相差会非常大。这里要说个例外如果在分布式场景下自增主键产生冲突因为多个节点各自生成ID可以考虑雪花算法生成的趋势递增ID而不是直接上UUID这样既保持了全局唯一也在一定程度上保留了顺序插入的优势。主键长度要尽量短因为每个二级索引的叶子节点都要存主键值主键长度越长二级索引占用的空间就越大。3.6 字符串索引要控制长度给字符串字段建索引不要直接整列建。比如文章表的标题字段title VARCHAR(200)直接在title上建索引B树每个节点能容纳的键值数量变少树变高IO次数变多。更好的做法是给前N个字符建索引也就是前缀索引。ALTER TABLE article ADD INDEX idx_title (title(20));前缀长度的选择依据还是区分度。可以这样测SELECT COUNT(DISTINCT LEFT(title, 10)) / COUNT(*) AS prefix_10, COUNT(DISTINCT LEFT(title, 20)) / COUNT(*) AS prefix_20 FROM article;找到区分度接近整列区分度的前缀长度。比如整列区分度是0.95前缀10的区分度就到0.93了那前缀长度就可以定为10。前缀索引的问题是它无法用于ORDER BY和GROUP BY操作因为排序需要完整值这一点要看实际查询是不是有这类需求。4. 实战拆解从业务需求到索引落地的完整流程4.1 先梳理查询场景再设计索引对于一个合格的索引设计流程第一件事不是写CREATE INDEX而是把所有将要执行的SQL语句整理出来。我会把高频SQL按模式分类等值查询、范围查询、排序查询、分组查询、模糊查询。这些不同的模式决定了索引应该怎么组织。整理SQL时不要只看单条语句要从业务角度出发。举个例子订单列表页面的核心场景是商家要查某段时间内的订单还要按状态筛选。整理出来的高频SQL可能是SELECT order_id, order_no, amount, status FROM order WHERE merchant_id ? AND create_time BETWEEN ? AND ? ORDER BY create_time DESC;这类SQL的索引设计我会拆开分析。merchant_id是等值条件create_time是范围条件ORDER BY create_time又额外需要排序。这里的最优解是联合索引(merchant_id, create_time)因为等值字段在前范围字段在后同时create_time已经在排序场景下天然有序ORDER BY可以直接利用索引顺序。另外一个关键点是查询字段order_id, order_no, amount, status是否都在索引里不在。所以还会考虑通过覆盖索引进一步优化后面的案例会单独讲。4.2 一个典型订单表从零设计索引的完整过程用一个更完整的例子走一遍。假设要设计一张订单表的索引表结构如下CREATE TABLE order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, merchant_id INT NOT NULL, user_id BIGINT NOT NULL, status TINYINT NOT NULL, amount DECIMAL(10,2), create_time DATETIME NOT NULL, PRIMARY KEY (id) );业务查询场景整理出来如下用户查自己的订单列表按时间倒序WHERE user_id ? ORDER BY create_time DESC商家查订单按状态时间筛选WHERE merchant_id ? AND status ? AND create_time BETWEEN ? AND ?根据订单号精确查订单WHERE order_no ?逐个设计。用户查订单列表索引建(user_id, create_time)。这样等值定位user_idcreate_time天然有序排序不需要额外的filesort。商家查询等值条件是merchant_id和status范围条件是create_time索引建(merchant_id, status, create_time)。最左前缀下(merchant_id)单独查也能命中这个索引所以这个索引同时覆盖了“按商家查全部订单”的场景。订单号查询给order_no建唯一索引因为业务上订单号必须唯一用唯一索引兼带约束功能。这个案例里应该能看出一个索引设计的思考方式每个索引都能解释自己覆盖了哪些查询解释不了的索引就删掉。4.3 覆盖索引优化带来的实测效果把上面的商家订单查询再优化一步。原来查询SELECT order_id, order_no, amount, status FROM order WHERE merchant_id ? AND create_time BETWEEN ? AND ?;在索引(merchant_id, create_time)下因为要select的字段order_no、amount并不在索引里所以每次查询都要回表。如果这个查询是页面上的高频操作回表的开销会积累得很可观。优化方案是调整索引为(merchant_id, create_time, order_no, amount, status)。注意索引列顺序等值条件的merchant_id在最前范围条件create_time次之最后加查询字段。这样查询的字段全部在索引中直接索引覆盖无需回表。实测数据量500万行时同样的查询回表版本平均耗时约120ms覆盖索引版本降到约30ms以内效果非常显著。不过这种写法要克制每加一个字段索引的存储空间和写入维护成本都在涨。简单说就是要把钱花在刀刃上只有高频查询才值得这样投入。4.4 表数据量大之后索引需要配合归档和重建数据量增长到一定级别即使索引设计合理查询也会因为索引树页面的老化、碎片化、统计信息的偏差而变慢。这时候需要考虑几件事一是对历史数据进行归档只保留热数据在业务表里表的数据量降下来B树的高度随之下降二是定期做ANALYZE TABLE更新统计信息防止优化器拿着过期的统计信息做错误的全表扫描决策三是对大索引做重建可以用ALTER TABLE ... ENGINE InnoDB来消除索引页碎片。这一条确实比较容易被忽略特别是在运维依赖自动化平台的公司里。索引设计好之后不是一劳永逸的它需要和数据生命周期管理配合。5. 哪些情况下索引会失效踩坑汇总索引失效是面试里高频得不能再高频的问题也是实际业务中最常见的故障来源。我整理一下我实际遇到过的失效场景。5.1 隐式类型转换最常见的是字段类型和传入参数类型不一致。比如user_id是VARCHAR类型但Java代码里传了数值类型WHERE user_id 123456。MySQL会对字段进行隐式类型转换导致索引失效走全表扫描。为何如此因为索引里面存的是字符串把它转换成数字去比较每一条索引记录都得做一次转换无法走B树的二分查找。反过来如果字段是数值类型传入字符串MySQL会尝试把字符串转成数值这种情况下索引通常还能用因为转换发生在参数端。所以规范是字段类型和参数类型必须严格统一。5.2 对索引列使用函数或运算WHERE DATE(create_time) 2024-01-01这种写法会让create_time上的索引失效。原因是B树里存储的是原始值对索引列做函数运算以后排序信息被打破优化器没法利用有序性。解决办法是改成范围查询WHERE create_time 2024-01-01 AND create_time 2024-01-02。同理WHERE amount 100 200也应该改写为WHERE amount 100。5.3 LIKE模糊查询的陷阱LIKE abc%可以利用索引LIKE %abc%则不能。因为B树是有序的前缀匹配还能用二分查找定位起点而中缀匹配无法确定起点位置只能扫全部。实际业务中如果确需中缀匹配可以考虑全文索引或者搜索引擎。拿热搜词里那个m3u8索引来联想一下如果一个视频点播系统要按m3u8文件名做模糊搜索且文件名是标识结构化的其实定义好前缀规则让业务走前缀匹配就能稳定命中索引。5.4 OR条件导致索引失效WHERE status 1 OR amount 100这种写法优化器如果决定对每个OR条件分别走索引然后合并结果那是可以的但实际中OR经常把优化器绕晕直接选择全表扫描。更稳妥的写法是用UNION ALL拆成两条SQL或者让业务明确两个条件的独立性。还有一个常见原因是OR条件的字段里有一个没索引整个查询就会放弃索引。单列索引后MySQL会用index merge机制合并两个索引的结果这个情况下还能用。但如果两个条件的字段都没索引那就只能全表扫。5.5 联合索引违反最左前缀这个前面详细讲过。直接用案例联合索引(a, b, c)查询里只有b和c没有a索引直接失效。这个坑我在代码评审里见过太多次了因为业务需求一旦变化新加的查询条件往往不会有人去检查它是否命中了现有索引的最左侧。5.6 优化器判断全表扫描更快这是个容易让人困惑的场景字段有索引查询也符合索引使用条件但执行计划就是不走索引。原因在于优化器根据统计信息估算如果某个字段的等值条件会过滤掉大多数行比如区分度很低的字段或者表数据量很少比如几百行优化器会判定全表扫描比走索引更快因为走索引需要随机IO回表总成本反而更高。这种情况不用慌不是索引设计有问题而是数据量小或统计信息该更新了或者需要考虑覆盖索引降低回表成本。6. 主键索引和唯一索引的区别与选型6.1 三个核心区别主键索引和唯一索引的区别我在项目评审里反复解释过整理成一张对比表维度主键索引唯一索引数量每张表最多一个每张表可有多个空值允许不允许NULL允许多个NULLMySQL中聚簇性InnoDB中是聚簇索引叶子节点存整行数据二级索引叶子节点存主键值默认约束自带唯一约束自带唯一约束用途组织表数据物理存储保证业务字段唯一性注意唯一索引允许多个NULL这一点经常被误解。MySQL的unique索引在遇到NULL时会认为“NULL不等于NULL”所以可以插入多行NULL值。如果业务需要“必须唯一且非空”还是要配合NOT NULL约束来做。6.2 业务唯一性校验的索引设计很多业务字段天然有唯一性要求比如用户表的手机号、订单表的订单号。这类字段应该用唯一索引而不是先查询再校验再插入。靠应用层先SELECT COUNT(*)判断有没有重复再决定插入会引入并发竞态条件两个请求同时通过校验然后同时插入如果库里没有唯一索引约束脏数据就进去了。唯一索引是数据库层面的最终防线比应用层的检查可靠得多。这里有一个性能细节唯一索引因为要保证唯一性插入时要做一次duplicate key检查性能比普通索引略低这个代价是值得的。而且不要无谓地在所有高频写入的表上加太多唯一索引每加一个写入的有效性检查就多一份。6.3 时空权衡唯一索引还是普通索引有些字段业务上“接近唯一但允许极少数例外”比如订单表中的platform_order_no大部分情况唯一极端场景可能为空或者历史数据里已经有少量重复此时无法直接建唯一索引建不了因为历史数据不满足唯一性。这个问题解决思路是清洗数据把重复数据处理掉再补唯一索引。数据库约束这层东西不该妥协。7. 索引失效与慢查询的排查手段7.1 EXPLAIN是必修课排查SQL问题第一件事永远是EXPLAIN。一条慢SQL执行计划里关注几个关键列type从好到差依次是system const eq_ref ref range index ALL看到ALL就要立刻警觉这是全表扫描的信号。key实际用到的索引名称。如果为NULL就是没走索引。rows预估扫描的行数。这个数如果接近全表行数说明过滤效果很差。Extra如果出现Using filesort和Using temporary说明排序和分组没利用上索引需要调整索引设计。如果出现Using index说明覆盖索引生效了是最理想的状态。有个细节值得提醒EXPLAIN的结果受统计信息影响表数据量变化大时要先ANALYZE TABLE再重新EXPLAIN不然看到的是失真数据。7.2 慢查询日志的打开方式打开慢查询日志是数据库日常巡检的标配。MySQL里可以临时开启SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;超过1秒的SQL就会被记录下来。慢查询日志的价值不在于事后救火而在于每周固定时间分析一轮把前10条耗时最高的SQL整理出来逐个看执行计划确认索引设计是否还是最优。很多线上事故如果在慢查询刚出现时就处理根本不会发展到数据库CPU打满。7.3 索引失效排查的完整思路当一条SQL走不上索引时按这个顺序排查看字段类型和参数类型确认有没有隐式转换。看SQL写法和索引列是否有函数、运算、LIKE中缀、OR条件。看联合索引的顺序是否满足最左前缀。看优化器估算的返回行数占比手动ANALYZE TABLE更新统计信息后再试。如果全部没问题但依然走不上把SQL里的索引列整理出来直接看能不能用覆盖索引改写降低回表成本引导优化器做选择。我见过不少团队在第五步之前就放弃了跑去调整MySQL参数最后也没解决问题。数据库参数调整是下策索引设计才是上策。8. 工具与习惯让索引设计成为低成本的事8.1 慢查询分析工具手工翻慢查询日志太痛苦我自己常用的是pt-query-digest它是Percona Toolkit里的一员能从慢查询日志中汇总出Top SQL和索引使用情况。它会把同类SQL聚合在一起按照总耗时排序让你一眼看到最该优化的SQL。分析能力其实不需要弄得很复杂关键是养成定期看的习惯。热搜词里出现的dbx数据库工具属于数据库管理类工具这类工具我没少用。它有图形化界面可以直连MySQL、Oracle等数据库查看表结构、索引信息、执行计划的效率远高于敲命令行。不过要强调的是工具只是辅助它不会替你做索引设计决策。真正做决策的还是你对业务查询模式的理解工具负责帮你把现状看得更清楚。8.2 索引评审纳入代码审查流程我的经验是在代码评审阶段就把SQL和索引一起看比上线后出问题再补救成本低得多。团队里应该有一个简单的约定任何新增或修改的SQL必须附带EXPLAIN截图key列使用到的索引要能在表结构中找到解释任何新增索引必须说明它覆盖了哪些业务查询。把索引设计变成开发流程的显式一环才能真正避免数据库被拖垮的那种事故。8.3 数据量预估驱动索引设计建表时就要对数据量有一个粗略的预估。日增1万行和日增100万行的表索引策略完全不同。数据量小的表甚至不需要二级索引全表扫描就够快数据量大的表索引设计差一点查询可能直接从毫秒级退化到秒级。做索引设计的时候把“这张表六个月后会多大”这个问题先回答掉很多设计决策会变得很清晰。9. 真实踩坑记录三个值得反复看的索引事故9.1 在状态字段上单独建索引早期我负责过一个工单系统工单表有个status字段取值是“待处理、处理中、已完成、已关闭”四种。当时遇到“查待处理工单”变慢的问题我直接给status建了一个索引。结果上线后查询更慢了EXPLAIN一看优化器压根没走这个索引因为status为“待处理”的行占比太高优化器判断全表扫描更快。后来做的调整是把SQL改成按主键范围分页扫描同时配合周期性任务把已完成和已关闭的工单归档到历史表让主表只保留热数据问题才彻底解决。这个案例的教训是低区分度字段单独建索引没有价值必须先缩小范围或者和其他高区分度字段组合成联合索引。9.2 冗余索引导致写入性能暴跌另一个项目里开发同学给订单表建了三个索引(merchant_id, status)、(merchant_id)、(status)。这个表每天有百万级写入订单数据量大索引也大结果就是每次插入一行数据要维护三个B树写入耗时直接上升了两倍多。排查后发现(merchant_id)完全被(merchant_id, status)覆盖删掉冗余的(merchant_id)索引后写入性能恢复。之后我们把冗余索引检查写进了每次上线前的checklist里。9.3 联表查询的驱动字段没索引还有个比较典型的案例是订单明细表关联商品表查询ON条件里的商品ID字段在明细表那边没建索引导致每次关联都要嵌套循环全表扫描。这种情况在写SQL的时候其实看着没啥问题但一跑就是几十秒。最后在关联字段上补了索引查询直接降到几十毫秒。联表查询的驱动字段索引经常被忽略尤其当表通过ORM维护时索引和表结构脱节的情况非常普遍。10. 索引命名规范和DDL操作的建议10.1 索引命名统一规范命名规范看起来是小事但对后期维护影响很大。我习惯的命名规则是普通索引idx_表名_字段名唯一索引uk_表名_字段名联合索引把字段名用下划线连接按索引顺序排列。比如idx_order_merchant_create_time。这套规则在索引数量多的时候非常有用看名字就知道这个索引的用途和列顺序排查问题时节省大量时间。10.2 大表加索引要选低峰期执行给几千万行的表加索引不是随便一条ALTER TABLE就完事。InnoDB在MySQL 5.6之后虽然支持在线DDLOnline DDL但ALTER TABLE ... ADD INDEX执行期间仍会产生额外的IO负载占用临时空间并对主从复制有延迟影响。操作要选业务低峰期预先估算临时空间需求加完索引后观察主从延迟。如果用的是早期的MySQL版本加索引会锁表那就更得谨慎别在白天直接动大表。这里顺便提一个思路不只是加索引修改表结构热搜词里就有“mysql数据库修改结构”在大表上同样要遵守“低峰期操作预先验证监控延迟”的原则。我通常会在预发布环境用同量级数据先跑一遍DDL记录耗时和锁表情况再决定线上怎么执行。10.3 用数据库巡检脚本固化索引健康度检查最后分享一个习惯我会定期跑一段SQL把数据库中所有表的所有索引大小、使用情况列出来。SELECT t.TABLE_NAME, s.INDEX_NAME, s.CARDINALITY, s.NON_UNIQUE FROM information_schema.STATISTICS s JOIN information_schema.TABLES t ON s.TABLE_NAME t.TABLE_NAME WHERE t.TABLE_SCHEMA your_database ORDER BY t.TABLE_NAME, s.INDEX_NAME;这段SQL能看出每个索引的基数Cardinality如果某个索引的基数很低说明这个索引的区分度很差值得怀疑是否该保留。把这段巡检固化到每月例行任务里很多隐患能提前发现。我个人在实际操作中的体会是索引设计没有银弹所有原则最后都要回到“读懂业务查询模式”这五个字上来。一次索引设计做得好不是用了多高深的技术而是把业务里的每个高频查询都认真拆解了一遍然后让索引精确地服务于查询路径。反过来那些动不动就全表扫描的系统往往不是技术能力不行而是从来没有认真做过这个拆解动作。最后再分享一个小技巧每次修改索引之后顺手把这条SQL的执行计划快照保存到表结构文档里三个月后再回来看你会发现这个动作有多值钱。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进