为什么多建索引能让查询变快,却会拖慢写入?

3 阅读14分钟

很多初学者第一次学性能优化,会有一个很自然的想法:既然索引能加速查询,那我多建几个不就更快了吗?

先说人话:可以更快,但不是白送的快。你把原本“查询时临时找”的工作,提前挪到了“写入时顺手整理”。所以读的时候像翻目录,刷一下就到了;写的时候像刚把一本书放上架,还得同步更新作者目录、主题目录、关键词目录,自然更慢。

这就是今天要讲的优化思想:用一部分写入性能,换更好的读取性能。最常见的手段,就是增加二级索引、为搜索建倒排索引、让高频查询尽量走覆盖索引。它特别适合读多写少、检索要求高的系统,但代价也很直接:写放大、存储占用增加,有时还会把写入延迟拖高。

如果你只想先记住一句话,那就是:索引本质上是在提前做功,读的时候省事了,写的时候就得多干活。

先把这笔账算明白:什么叫“用写换读”

术语先别急,我们先看一个生活场景。

你开了一家仓库,里面有十万件货。没有目录时,找货就得一排排翻;如果你提前做了品牌目录、颜色目录、仓位目录,找货会很快。但每次新货入库时,你不只是把货放进去,还得把三本目录一起更新。

数据库和搜索系统也是这套逻辑。

  • 读取性能:系统把你要的数据找出来的速度。

  • 写入性能:系统把新数据写进去,或把旧数据改掉的速度。

  • 用写换读:写入时多维护几份“目录”或“检索结构”,换来读取时少扫描、少排序、少回表。

来看一张总流程图。


业务提出:查询要更快

-> 先判断:是不是读多写少

-> 再判断:是不是经常按条件过滤、排序或关键词搜索

-> 是:增加合适的索引结构

-> 写入时:主数据之外,还要同步维护这些索引

-> 读取时:直接走目录定位,少扫大量原始数据

-> 结果:查询更快,写入更慢,存储更大

这张图说明的不是“该不该盲目建索引”,而是你应该先确认业务是不是典型的“读多写少”。

为什么索引会让读取变快

很多新手把索引理解成“一个加速按钮”,其实更准确的说法是:索引是系统提前整理好的查找路径。

没有索引时,系统常常得把整张表扫一遍,像在一堆纸箱里挨个翻。

有索引时,系统可以先看目录,再去少量位置拿结果,像先查商场楼层导览,再直奔店铺。

1. 二级索引:额外做一本按条件查的目录

术语先讲人话:二级索引,就是主键顺序之外,按别的字段再建一本目录。

生活类比:图书馆书架是按编号摆放的,但你还想按作者找书,于是管理员额外做了一本“作者目录”。

小场景:客服天天按用户编号和订单状态查最近订单,这时在 user_idstatuscreated_at 上建立组合索引,查询就会轻松很多。

比如一个订单列表页,最常见的查询可能长这样:


select id, user_id, status, created_at, amount

from orders

where user_id = 1001 and status = 'paid'

order by created_at desc

limit 20;

如果没有合适索引,数据库可能要扫很多订单,再做过滤和排序。

如果有按 user_idstatuscreated_at 组织好的二级索引,它就像先拿到了目录卡,能更快缩小范围。

2. 倒排索引:把“文章在哪些词里出现”反过来记

术语先讲人话:倒排索引,就是不再记“这篇文档里有什么词”,而是记“这个词出现在哪些文档里”。

生活类比:一本厚书最后的关键词索引页,不是按章节列词,而是按词列页码。

小场景:你做了一个知识库,用户搜索“覆盖 索引”,系统如果每次都把所有文章全文扫一遍,速度会很难看;倒排索引会直接告诉系统,这两个词分别出现在哪些文档里,再做交集就快多了。

举个极小的示意:

  • 文档 1:覆盖索引入门

  • 文档 2:倒排索引适合什么场景

  • 文档 3:订单列表为什么查得慢

倒排索引会更像这样记:

  • 覆盖索引 -> 文档 1

  • 倒排索引 -> 文档 2

  • 场景 -> 文档 2

  • 订单 -> 文档 3

于是搜“覆盖索引”,系统先看词表,不用从 1 到 3 一篇篇读。

这就是为什么全文检索、站内搜索、商品搜索,几乎都离不开倒排索引。

3. 覆盖索引:连回表都省了

术语先讲人话:覆盖索引,就是查询要的列,索引里已经全有了,系统不必再回到主表取一次数据。

生活类比:你本来只是想看图书目录上的书名、作者、馆藏状态,如果目录卡上已经写全了,就没必要再去书架把书拿下来确认。

小场景:运营后台的订单列表页,只展示下单时间、状态、金额三个字段。如果这些字段都在索引里,列表页就能直接从索引返回结果,少走一趟主表。

还是上面的订单查询,如果你希望列表页尽量走覆盖索引,常见思路是让高频查询要用到的列尽量包含在索引里:


create index idx_orders_user_status_ctime

on orders(user_id, status, created_at);

  


create index idx_orders_cover

on orders(user_id, status, created_at, amount);

这里不是让你背语法,而是看懂思路:

第一条索引解决“按谁、什么状态、按时间排”;

第二条索引更进一步,尝试把金额也放进来,让某些列表查询不用再回主表。

当然,索引列不是越多越好。索引一胖,写入和存储压力也会跟着胖起来,像健身房办卡办太多,钱包先瘦下来。

补一句定位:二级索引和覆盖索引常见于关系型数据库,倒排索引更常见于搜索引擎和全文检索系统,但背后的交换逻辑是一回事。

三种手段到底在换什么

先用一张表把差异摆平。

| 手段 | 人话理解 | 最适合的需求 | 读取收益 | 写入代价 | 额外存储 |

|---|---|---|---|---|---|

| 更多二级索引 | 多做几本按字段查的目录 | 精确过滤、范围查询、排序 | 常常明显提升 | 每次写入都要更新更多索引 | 增加 |

| 倒排索引 | 按关键词反查文档位置 | 全文检索、关键词搜索 | 对搜索类场景提升很大 | 分词、合并、更新索引都要成本 | 通常增加更多 |

| 覆盖索引 | 让查询直接从索引拿结果 | 高频列表页、固定字段查询 | 不仅能定位,还能少一次回表 | 索引更宽,维护更重 | 增加 |

这张表的作用是帮你先选方向:按字段筛选优先看二级索引,按关键词搜索优先看倒排索引,固定列表页再看覆盖索引。

一遍走通:订单系统怎么把“写换读”用明白

我们用一个初学者最容易代入的例子来走一遍。

假设你有一个订单系统,平时有三类典型读取:

  1. 客服按用户编号和订单状态查最近 20 条订单

  2. 运营看列表页,只关心时间、状态、金额

  3. 用户在备注里搜“退款”“改地址”这样的关键词

如果你什么都不做,这三类查询可能都慢,因为系统每次都在现场找东西。

于是你开始做优化。

第一步:先给高频过滤条件建二级索引


create index idx_orders_user_status_ctime

on orders(user_id, status, created_at);

结果是什么?

  • 客服按用户和状态查最近订单,通常会快很多

  • 因为索引里已经按条件组织好,数据库更容易直接定位结果范围

  • 但每新增一条订单,除了写主表,还要写这条索引

第二步:再看列表页能不能走覆盖索引

如果列表页固定只展示 created_atstatusamount,那你可以考虑让这些字段尽量被索引覆盖。


select created_at, status, amount

from orders

where user_id = 1001 and status = 'paid'

order by created_at desc

limit 20;

这类查询如果能直接从索引返回结果,就会少一次回表动作。

少一次回表,在高频列表页上经常很值钱,尤其是数据量大时。

第三步:关键词搜索不要硬靠普通索引扛

如果客服还想搜索备注里的“退款失败”“地址错误”,这时普通二级索引通常帮不上大忙。

因为它更擅长按字段值查,不擅长在大段文本里找词。

这时就该上倒排索引思路,把备注或全文字段交给搜索结构处理。

查询时像查关键词目录,写入时则要额外做分词、更新词项列表,所以写成本会继续往上走。

你可以把整个写入链路想成这样:


写入一条订单

-> 写主表

-> 更新 idx_orders_user_status_ctime

-> 更新覆盖列表页需要的索引

-> 如果备注支持关键词搜索,再更新倒排索引

-> 写入完成

这条链路越长,写入越不可能像裸奔一样轻快。你下一步该做的,不是继续加索引,而是先数清楚一条写入到底维护了几份结构。

什么时候值得这样做,什么时候别冲动

不是所有系统都适合“用写换读”。

你在下面这张决策表里,基本能找到自己的位置。

| 业务条件 | 更多二级索引 | 倒排索引 | 覆盖索引 |

|---|---|---|---|

| 读多写少 | 适合 | 适合搜索场景 | 很适合高频列表页 |

| 写多读少 | 谨慎 | 更要谨慎 | 谨慎控制列数 |

| 经常按字段过滤和排序 | 优先考虑 | 不解决这个问题 | 可搭配 |

| 经常按关键词搜标题、正文、备注 | 帮助有限 | 优先考虑 | 不是主角 |

| 查询字段固定、返回列少 | 可搭配 | 不关键 | 优先考虑 |

| 存储预算紧张 | 谨慎 | 谨慎 | 谨慎 |

| 写入延迟要求非常严 | 少建 | 少建 | 少做胖索引 |

这张表想告诉你的重点很简单:先看业务读写比例和检索方式,再决定建什么索引,不要一上来就把所有字段都点成“加速”。

代价到底有多真实:写放大不是吓人的词

很多文章提“写放大”,初学者一看就觉得像考试名词。我们把它翻成大白话。

术语先讲人话:写放大,就是业务上看起来只写了一次,底层却写了很多次。

生活类比:你只是通知了一件事,但要同时抄到总账、分类账、日报表、备忘录里。

小场景:你改了一条订单的状态,主表要改,按状态查的索引要改,按用户和状态查的索引要改,如果搜索里也能按状态过滤,相关结构还可能继续改。

所以“写 1 次”在系统内部可能变成:

  • 写主数据 1 次

  • 更新二级索引 2 次或更多

  • 更新覆盖索引 1 次或更多

  • 更新倒排索引若干次

这就是写放大。

它带来的直接后果通常有三个。

1. 写入变慢

每加一个索引,写入路径就多一个维护动作。

插入慢一点,更新慢一点,删除也慢一点。高并发写入时,这种慢会被放大得更明显。

2. 存储占用增加

索引不是空气,它要落盘。

二级索引要存键和值的映射,覆盖索引因为列更多会更胖,倒排索引还要存词项和文档列表。数据量一大,磁盘就会很诚实。

3. 维护复杂度上升

索引一多,优化空间确实大了,但排查问题也更难了。

你得判断哪些索引真在用,哪些是重复建设,哪些只是心理安慰。

再看一张“优化前后”的账单表,会更直观。

| 维度 | 索引少或没有 | 二级索引较多 | 再叠加倒排与覆盖索引 |

|---|---|---|---|

| 精确查询速度 | 慢 | 快 | 快 |

| 关键词搜索速度 | 慢 | 仍然一般 | 很快 |

| 写入速度 | 快 | 变慢 | 更慢 |

| 存储占用 | 低 | 中 | 高 |

| 适合场景 | 写多读少 | 读多写少 | 检索要求高、读远多于写 |

这张表的意思很朴素:别只盯着“查快了”,还要同时盯“写慢了多少、盘多占了多少”。

初学者最容易踩的 4 个坑

坑 1:以为索引越多越好

不是。

索引是收费通道,不是免费礼包。每多一个索引,写入都要多交一次过路费。

坑 2:把低区分度字段单独建得很开心

像性别、是否删除、是否启用这种字段,如果只有极少几个值,单独建索引不一定划算。

系统可能发现“筛完还是一大堆”,那这本目录就没你想得那么值钱。

坑 3:为了覆盖索引,把大字段也塞进去

覆盖索引的目标是减少回表,不是把索引养成胖子。

把长文本、大字段硬塞进去,常常会让存储和写入成本先爆炸。

坑 4:用普通二级索引硬扛全文检索

普通索引擅长按字段值找,不擅长在大段文本里做复杂关键词匹配。

遇到搜索场景,老老实实考虑倒排索引,别让锤子去拧螺丝。

实战里怎么判断“该不该用写换读”

如果你是刚入门,可以直接按下面 5 步判断,不容易跑偏。

  1. 先找高频查询

看哪些查询最常出现,别为低频页面堆一堆索引。

  1. 再看读写比例

如果一天读十万次、写一千次,适合认真做索引;如果反过来,先谨慎。

  1. 判断查询类型

按字段过滤和排序,优先看二级索引;按关键词搜全文,优先看倒排索引;固定列表页,考虑覆盖索引。

  1. 估算写入代价

每新增一个索引,都问自己一句:每次插入、更新、删除,会不会都要额外维护它?

  1. 用执行计划和真实指标验证

别靠感觉觉得“应该会快”。要看慢查询、执行计划、写入延迟、索引大小,再决定留不留。

你会发现,真正专业的优化不是“多建”,而是“建得准、建得少、建得值”。

一段给初学者的答疑

问:只要是读多写少,就一定要多建索引吗?

不一定。先看是不是高频查询、是不是核心路径、是不是确实慢。读多写少只是适合优化,不代表要把每个字段都建一遍。

问:覆盖索引是不是一定比普通索引高级?

也不是。它只是更适合“返回列固定而且不多”的场景。列太多时,收益可能不如成本。

问:倒排索引能替代数据库普通索引吗?

不能。倒排索引擅长搜索,二级索引擅长精确过滤、范围查询和排序,它们解决的问题不同。

问:我该先优化哪一个?

先优化最常被用户感知到的慢查询,尤其是列表页、详情页、搜索页这三类高频入口。

最后记住 5 句话

  • 先检查读写比例,再决定要不要用写入性能换读取性能。

  • 先选择合适的索引类型,再动手建索引,不要把不同问题都交给同一种工具。

  • 先测高频查询,再补二级索引或覆盖索引,别为低频场景背长期成本。

  • 先区分字段检索和全文搜索,遇到关键词搜索就认真考虑倒排索引。

  • 先验证写入延迟、存储占用和执行计划,再确认这次优化到底值不值。

如果把这套思想真正吃透,你以后看到“加索引提速”这句话,就不会只想到快,而会马上想到另外半句话:快,是拿写入和存储换来的。那时你就不是在背概念,而是在做工程取舍了。