ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

后端性能优化,往往从数据库索引开始

后端性能优化,往往从数据库索引开始 慢查询日志第一页赫然躺着一条耗时12秒的SELECT。你加了个索引用时变成0.03秒——这就是后端性能优化的全部隐喻大多数“高并发”问题的真相不是服务器不够多而是数据库在裸奔。从业十年我见过太多团队在Redis、消息队列、微服务拆分上砸钱砸人力最后发现性能瓶颈就卡在一张订单表上——没有索引或者索引建得稀烂。后端性能优化往往从数据库索引开始因为索引是唯一能让查询从“扫描全表”变成“按图索骥”的机制。这不是什么高深理论而是最底层的生存法则。慢从来不是服务器的错每当接口变慢第一反应通常是“加机器”“上缓存”。但缓存的命中率再高也总有穿透到数据库的一刻机器再多也架不住一条查询把一个亿的行全部读进内存再逐行扔。真正让后端变慢的不是CPU不够快、内存不够大而是数据库在查询时根本不知道你要什么只能把整个表翻个底朝天。全表扫描是数据库最无奈的选择而索引就是告诉数据库“你要的数据在B树第几层哪个叶子节点”的地图。我调过一个支付系统晚上八点高峰时接口超时率高达30%。DBA抓出慢SQL发现是一条按用户ID查最近订单的语句而订单表有五年历史数据七千多万行。当时的“优化方案”是加两台只读从库结果只缓解了半小时——因为每条查询在每个从库上同样要全表扫描。后来加了一个(user_id, created_at)联合索引超时率直接归零。没有索引再多数据库副本也是在重复做无用功。索引的本质是数据结构它把无序的数据组织成可快速查找的形态。理解这一点你就明白了为什么索引能带来数量级的提升全表扫描是线性复杂度O(n)而B树查找是O(log n)。当数据量从一万行涨到一亿行全表扫描的代价涨了一万倍而索引查找只多走了几次磁盘IO。这个数学差距决定了数据库性能的分水岭永远在索引设计上。索引不是随便建几个字段就行很多开发者的索引观停留在“把WHERE条件里的字段都建上索引”。这是典型的自杀式优化。索引不是越多越好而是越精准越好因为每一个索引都是写操作的额外负担。一张表如果有十个索引每插入一行数据就要同时维护十棵B树。在写多读少的场景下过度索引会让系统的吞吐量雪崩。真正高效的索引设计必须遵循一条铁律索引要尽可能窄且要和查询模式完全对齐。你建了一个包含五个字段的联合索引但你的查询只用到其中两个字段那么这个索引有一半的空间是浪费的而且它不会命中最优路径。更糟糕的是如果你建的索引顺序和查询条件不匹配数据库可能完全用不上这个索引照样全表扫描。我在审查过的一个电商后台里看到过这样的“杰作”给status字段只有三个值0、1、2单独建了索引。由于区分度极低查询优化器评估后认为走索引还不如全表扫描于是索引成了摆设。低区分度的字段单独建索引本质上是给数据库增加写放大却换不来任何读加速。正确做法是把它放进联合索引的最后一个位置或者干脆不建。最左前缀一半程序员栽在这里联合索引的规则是“最左前缀原则”但多数人只是背了口诀没懂背后的几何意义。联合索引(a, b, c)实际上是先按a排好序在a相同的前提下再按b排序最后在b相同的前提下才按c排序。这意味着如果你的查询条件不包含a那么整个索引结构就乱掉了——因为它不是按b或c全局有序的。所以where b 1 and c 2命不中这个索引而where a 1或where a 1 and c 2能命中后者只能用到a部分。更隐蔽的陷阱是范围查询。如果你的查询是where a 100 and b 1即使在联合索引里b部分也无法用于定位——因为一旦a是范围b的有序性就只有在a相等时才成立。范围查询右边的字段索引会直接失效。这就是为什么很多慢查询在执行计划里显示“用到了索引”但rows依然很大——因为只用了索引的一小部分其他字段只是在索引基础上做过滤。索引排序是另一个容易被忽视的杀手。当查询需要ORDER BY created_at DESC时如果联合索引的顺序不对数据库只能把结果集加载到内存里做filesort。一旦结果集超过sort_buffer_size就会产生磁盘临时文件性能瞬间崩塌。聪明的索引设计应该让ORDER BY直接走索引的有序性避免额外的排序步骤。这意味着你不仅要考虑WHERE条件的字段顺序还要把排序字段放在索引末尾并且和排序方向一致。覆盖索引免费的午餐但你要会点覆盖索引是索引优化里最高性价比的技巧没有之一。当索引中包含的字段已经覆盖了查询所需的所有列时数据库根本不需要回表直接从索引树里拿数据。这个“不回表”的收益极其可观一次查询从两次IO变成一次IO而且索引通常比表小得多缓存命中率也高得多。举个例子一个查询SELECT id, user_id FROM orders WHERE user_id 123如果你在user_id上建了普通索引那么索引叶子节点存储的是主键id查询过程需要先找到主键再回表读取整行从中取出id和user_id。但如果你建立一个联合索引(user_id, id)这个索引本身就包含了user_id和id查询可以直接返回连回表的动作都省了。注意这个技巧的关键是“查询的列”而不是“WHERE的列”——很多人把SELECT的字段忘得一干二净。覆盖索引还能化解一个经典难题大字段表的count查询。如果表里有个content字段你执行SELECT COUNT() FROM articles WHERE author_id 5普通索引可能因为要回表而变慢但一个(author_id, id)的覆盖索引让COUNT变成纯粹的索引扫描速度提升几十倍。记住覆盖索引就是用空间换时间但这个小代价换来的是数量级的速度提升绝对划算。索引下推数据库偷偷帮你干的活你可能没听过“索引下推”但MySQL 5.6之后它一直在默默改善你的查询。在没有索引下推的年代联合索引(a, b)对于WHERE a LIKE abc% AND b 1这样的查询只能根据a的模糊匹配找到一批主键然后回表逐行检查b是否等于1。这个“回表后再过滤”的动作让索引的效率大打折扣。有了索引下推数据库在索引遍历的过程中就先对b字段进行判断把不满足条件的记录直接从索引层面剔除只有真正满足条件的才回表。这相当于把过滤操作下推到存储引擎层减少回表次数和IO开销。但索引下推不是万能药——它要求被下推的字段确实存在于索引中否则无从判断。所以联合索引的设计要尽量把等值判断的字段放在前面把模糊匹配的字段放在后面这样下推才有施展空间。这个细节告诉我们索引优化不是建完索引就结束了还要了解数据库的查询优化器如何工作利用它的特性事半功倍。毕竟现代数据库的优化能力比大多数人想象的更强但前提是你的索引结构给了它优化的机会。写多读少警惕索引的暗面前面讲的全是读优化但索引有它的阴暗面每一次写操作INSERT/UPDATE/DELETE都必须同步更新所有相关索引这会让写放大成数倍。在日志型、事件型、消息队列落库这类写密集场景里索引数量直接决定了写入吞吐量的上限。我接手过一个物联网数据采集服务每秒要写入上万条设备上报数据。原设计在device_id、timestamp、data_type各建了一个单列索引结果写入性能惨不忍睹磁盘IO严重饱和Kafka堆积告警。分析后发现业务查询只有一种——按设备时间段聚合所以三个单列索引完全可以合并成一个联合索引(device_id, timestamp)其余索引全部删掉。这样不仅写入速度翻倍查询也更快因为联合索引本身就是为查询而生的。更极端的场景是如果表引擎是每次写都追加的日志表查询需求极其罕见那么干脆不要建任何索引。没有索引写入就是纯追加速度堪比顺序写文件。等真正需要查询时再花几秒钟建一个临时索引。性能优化的本质是取舍而不是无脑加索引——有时候什么都不建才是最优解。索引设计一场与业务的对赌索引设计本质上是一场与业务的对赌你赌的是未来半年的查询模式。你要在写代码之前不是先写接口而是先写查询语句——所有的查询模式、频率、响应时间要求都决定了索引的形状。常见的反模式是“先建表后补索引”等到接口上线了发现慢查询了再临时加索引。往往到了这一步你已经被性能问题绑架加索引要锁表锁表要停机停机要看业务脸色最后只能半夜偷偷加。高手的做法是在建表时就设计好索引而且会模拟真实查询走一遍执行计划。你要问自己三个问题这个查询的WHERE条件有哪些它的排序和分组字段是什么SELECT的列能否被覆盖索引包住然后把答案映射到联合索引的字段顺序上等值条件放在最前面范围条件次之排序字段紧随其后最后放覆盖用的宽字段。这个顺序是无数实践验证过的黄金法则。但别指望一次性设计出完美的索引。业务是活的数据分布是变的今天区分度高的字段明天可能全表都是同一个值。所以索引优化不是一锤子买卖而是持续观察慢查询日志和执行计划越改越合理的过程。成熟的团队会把“索引评审”纳入代码评审的环节每次SQL变更都要附带explain结果从源头掐死性能隐患。让执行计划做你的判官说了这么多理论最终都要落到一个动作上看执行计划。在MySQL里就是explain在PostgreSQL里就是EXPLAIN ANALYZE。它告诉你数据库到底用的是全表扫描还是索引扫描、它预估会扫描多少行、有没有使用临时表、有没有filesort。所有关于索引的争论在执行计划面前都不值一提——数据说了算。常见的问题是你明明建了索引但执行计划显示没用。这时候别急着骂数据库先检查三件事一是字段隐式类型转换比如字符串列用数字去比较索引会失效二是函数包裹WHERE DATE(created_at) 2024-01-01让索引形同虚设改成created_at 2024-01-01 AND created_at 2024-01-02即可三是like查询的前置通配符%abc无法走索引abc%可以。这些都是基础中的基础但很多人踩坑踩到怀疑人生。最后说一个扎心的事实索引优化的天花板就是数据库单机性能的上限当索引已经无可优化时再往上的性能就必须靠架构来解决——分库分表、读写分离、数据归档。但反过来如果你的索引还没做好就急着上分库分表那只会把一个全表扫描的问题放大成十个全表扫描。所以请永远记住后端性能优化往往从数据库索引开始也必须在索引上打好地基。地基不稳上层建筑再华丽也是危楼。
返回列表