ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

提升后端性能,先学会优化数据库查询

提升后端性能,先学会优化数据库查询 凌晨三点监控大屏上那根刺眼的红线还在往上爬。后端服务的CPU飙到90%数据库连接池被占满响应时间从200ms一路狂飙到3秒。你打开慢查询日志发现罪魁祸首是一条跑了4.2秒的SQL——它不过是想查一张三千万行的订单表里某个用户的最近十条记录。这就是后端性能崩塌最常见的起点不是代码逻辑不够高效而是数据库查询在无声地吞噬一切。很多团队把性能优化寄托在增加缓存、堆机器、上消息队列上却忽略了一个最基础也最致命的事实所有缓存最终都要回源数据库所有微服务最终的瓶颈都在数据层。如果你不会优化查询加再多Redis也只是把问题往后推迟而且会让缓存击穿、穿透、雪崩来得更猛烈。真正的高手首先会把SQL打磨得像手术刀一样精准。慢查询是性能问题的放大器不是病根当你看到一条慢SQL第一反应不应该是“优化它”而是“它为什么这么慢”。数据库的执行过程——解析SQL、生成执行计划、执行索引扫描或全表扫描、回表取行、排序、分组、JOIN——每一步都可能成为瓶颈。慢查询日志里记录的是现象执行计划里藏着原因。用EXPLAIN看一条查询如果看到type列是ALL或者rows预估上百万就说明优化器决定全表扫描这才是你该动手的地方。更隐蔽的是那些单次执行只要几毫秒、但每秒被调用上千次的查询。它们不会出现在慢查询日志里却能把数据库的IOPS撑爆。衡量查询好坏的标准不是单次耗时而是总资源消耗。一个返回100行但扫描10万行的查询和一个精准命中索引返回10行的查询对数据库的压力天差地别。你需要的不是对所有SQL一视同仁而是建立一套分级监控体系慢日志抓长尾性能监控抓高频两者结合才能定位真正的毒瘤。索引不是越多越好而是越准越好很多人给表建索引像撒胡椒面看到WHERE条件就建一个看到ORDER BY又建一个。结果索引比数据还大写入性能急剧下降优化器反而在多个索引之间犹豫不决。索引的本质是空间换时间但更准确地说是用预排序的结构换查询时的随机IO。B树之所以成为数据库的默认索引结构是因为它能以log(N)的复杂度定位数据并且叶子节点天然有序能高效支持范围查询和排序。真正有效的索引一定是根据查询模式设计的。你得先问自己这条查询最频繁的过滤条件是什么结果集需要什么样的顺序覆盖索引能不能避免回表一个经典的经验法则是最左前缀匹配选择性高的列放在前面范围查询放在最后。但要记住这不是死板的教条——如果某列的区分度极低比如性别只有两个值把它放在联合索引最前面就是浪费。实践中最靠谱的方法是把生产环境的慢SQL收集起来逐条分析其WHERE、GROUP BY、ORDER BY、JOIN条件然后设计出能同时服务多条查询的复合索引。覆盖索引让你的查询告别回表之痛假设你有这样一条查询SELECT id, title, status FROM articles WHERE author_id 100 AND status 1 ORDER BY created_at DESC LIMIT 10。常规索引是(author_id, status)执行时通过索引找到符合条件的主键再每行回表去读title、created_at最后排序、取10条。如果数据行很大回表带来的随机IO会让性能直线下降。而如果将索引建成(author_id, status, created_at, id, title)查询需要的所有列都在索引里数据库无需回表就能直接返回结果。这叫做覆盖索引是查询优化里性价比最高的手段之一。覆盖索引的妙处在于它把索引当成了一个精简的“物化视图”。尤其在统计类查询里比如SELECT COUNT() FROM orders WHERE status 2如果(status)是索引COUNT()可以直接扫描索引而不是全表速度会快几个数量级。设计覆盖索引的原则是查什么列就尽量让索引包含什么列。但要注意索引列不是越多越好因为每一列都会增加写入成本和索引存储空间。选择那些查询最频繁、回表代价最大的列来覆盖才是明智之举。别再写那些让索引失效的查询了技术社区流传着很多“让索引失效的写法”大部分是准确的。比如在WHERE条件中对索引列使用函数WHERE DATE(created_at) 2024-01-01这会让索引失效因为优化器需要对每一行的created_at先计算DATE再比较。正确的写法是WHERE created_at 2024-01-01 AND created_at 2024-01-02。范围查询要能走到索引就得遵循“等值在前、范围在后”的顺序。还有隐式类型转换WHERE phone 13800001111如果phone是varchar那这个数字会被转成字符串——一旦索引列被转换索引就报废了。这些细节看似简单但在真实代码里比比皆是。我曾经见过一条线上SQL因为一个字段用了IS NOT NULL判断导致该列索引完全失效本来0.1秒的查询变成2秒。优化查询很多时候不是在创造新东西而是在清除代码里的愚蠢。另外LIKE %关键词%这种前后通配符的模糊查询天然无法使用B树索引——除非你引入全文索引或搜索引擎。把这些常识内化成习惯比学任何高级技巧都管用。JOIN优化别让笛卡尔积偷偷爆炸多表连接是后端性能黑洞的高发区。很多新手写JOIN时不关注连接顺序也不看驱动表的行数结果数据库不得不对几十万行和几百万行做嵌套循环每条SQL都像在燃烧CPU。优化的核心原则有两条用小表驱动大表连接字段必须有索引。在MySQL的嵌套循环连接Nested Loop Join中驱动表是外层循环被驱动表的连接列上如果没有索引每次匹配都相当于全表扫描——这绝对是不可接受的。实践中你应该在EXPLAIN结果里看哪个表是驱动表哪个表被驱动。如果被驱动表的连接列上没有索引马上加上。如果是关联查询返回结果过大比如一对多关系可以考虑先聚合子表再和主表JOIN。但有时候更彻底的优化是拆掉JOIN——在业务代码里分两次查询第一次查出主表数据第二次用主表ID列表去查子表然后在内存中组装。这样做的优势是每个查询都简单、高效也便于利用Redis等缓存。记住数据库最擅长的是单表查询和简单索引查找复杂的业务组装应该交给应用层。分页越深性能越差你得换种翻页姿势LIMIT 100000, 10这条查询会让数据库先扫描前100010行然后丢弃前100000行只返回最后10行。这个“丢弃”的过程带不来任何收益却消耗了巨大的IO和CPU。分页优化的核心思路是不要让数据库去扫描你根本不需要的行。一种经典做法是“延迟关联”先查出目标页码的主键ID然后再用ID关联原表取出完整数据。比如把SELECT FROM orders ORDER BY id LIMIT 100000, 10改成SELECT FROM orders JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) t ON orders.id t.id这样内层查询只扫描主键索引而不是把整行数据都读出来效率提升会非常明显。另一种更实用的方法是用“游标分页”替代“偏移分页”。前端传来上一页最后一条记录的ID或时间戳查询时用WHERE id last_id ORDER BY id LIMIT 10数据库可以直接走索引定位到last_id然后往后取10条。这样无论翻多少页查询耗时都恒定在极低的水平。缺点是用户不能随意跳页但对于无限滚动流的业务场景如Feed流、搜索历史这是最优雅的解法。在表数据量达到千万级别后任何基于OFFSET的分页都该被列入黑名单。别把数据库当计算器也别让它做它不擅长的事很多后端性能问题的根源是把数据库当成了万能工具。比如在SQL里做复杂的正则表达式匹配、JSON字段的深度解析、复杂的数学计算。这些操作不仅无法利用索引还会严重占用数据库的CPU。数据库最擅长的是“存取”和“简单过滤”而不是“计算”。如果一个字段需要经常提取JSON里的某个键值你该考虑把它提取成独立的列或者直接使用文档数据库。与此类似SELECT也是性能杀手。它不仅多传了很多无用数据还会增加网络传输、内存消耗更重要的是让覆盖索引失效。写出具体的列名既是优化也是一种良好的工程习惯。还有一个容易被忽略的点在事务里执行长查询或大批量更新。事务里的长查询会持有锁阻塞其他事务导致数据库并发能力直线下降。把大事务拆成小事务避免一次性更新百万行这些对查询性能的间接帮助往往比改一条SQL更大。缓存你的查询结果但要设好失效边界查询优化到极致后如果某个热点数据依然被反复查询就该考虑查询结果缓存了。但缓存不是银弹它需要在数据一致性、内存占用、缓存命中率之间做权衡。对于读多写少、实时性要求不高的数据比如文章内容、商品详情用Redis缓存JSON结果完全没问题但对于库存、余额这类强一致性的数据缓存可能带来一堆麻烦。一种更精细的玩法是缓存查询所需的主键或ID列表而不是缓存最终结果。当用户请求列表页时先从缓存拿到ID列表再通过主键批量查询数据库并且可以单独缓存每个实体。这样即使其中一条数据更新了也只影响该ID的缓存而不用把整个列表缓存清掉。缓存永远要设置过期时间和最大容量更要在数据库更新时主动失效对应缓存否则你会在某个深夜被数据不一致的Bug叫醒。用数据库设计反推查询优化有时候单条查询怎么优化都绕不开昂贵的扫描原因出在表结构设计上。一个典型的反例是“EAV实体-属性-值”设计把正常的行拆成多行键值对查询时要做大量自连接性能极差。设计表的时候应该优先考虑业务查询的访问路径——你将来要怎么读这些数据就怎么设计存储。垂直拆分将热点列和冷数据分表和水平分表按时间或ID范围分片都是应对超大表的常用手段。但分表会引入跨表查询、全局排序、分布式事务等复杂度必须谨慎决策。在分表之前先审视你的查询是否真的需要全表数据——很多时候归档旧数据、清理无用字段就能让主表瘦身查询自然加速。数据库不是垃圾场别把所有东西都塞进去却不考虑如何取出来。设计阶段多花五小时运行起来能省五十小时。构建你的SQL优化闭环优化数据库查询不是一个一次性的动作而是一个持续的过程。你需要一套完整闭环采集慢日志、分析执行计划、优化索引和SQL、验证效果、监控回归。每个季度都应该做一次“数据库体检”找出那些读写比失衡、索引冗余、查询模式变化的表重新设计优化策略。同时把查询规范写进团队的代码评审清单。比如禁止SELECT 、禁止无索引的JOIN、禁止在索引列上使用函数、分页必须用游标等。让每个开发者在写SQL的第一秒就带着性能意识比事后依托DBA救火要有效百倍。还要建立性能回归测试在发布前用压测工具模拟真实的查询负载看看新上线的代码是否会给数据库带来压力。当团队成员都能熟练解释EXPLAIN输出并主动设计覆盖索引时你的后端性能已经赢在了起跑线上。数据库查询优化本质上是对数据访问方式的深刻理解。它不需要你背诵奇技淫巧只需要你尊重索引的结构、理解执行计划的逻辑、洞察业务数据的访问模式。每一次优化的落点都是减少数据库的无效工作量——少扫描一些行少回一些表少做一次排序。当你把这条原则贯彻到每一行SQL里后端性能提升是水到渠成的事。那些在凌晨爬起来处理慢查询的滋味希望你永远不要再尝到。
返回列表