资讯详情

资讯详情

数据库性能优化实战:用SQL优化与索引设计实现10倍提升

我先说一句可能得罪人的话在绝大多数业务系统里数据库性能差真不是硬件不够也不是某个参数没调好。最常出问题的恰恰是SQL本身的写法、索引的缺失、以及表和业务长期演化后留下的结构负担。标题里的“10倍”乍看像博眼球的数字但只要你把一个跑了3秒的查询压到300毫秒再把几个高频接口背后拖垮全库的慢SQL清掉整体体验提升一个数量级是真实发生过的。尤其是那些上线了两三年、表数据量翻了十几倍的系统优化空间比我见过的大多数“新项目”还要夸张。这篇东西适合谁一类是被慢查询困扰的开发、运维、DBA一类是刚接手老系统、想快速抓到重点的技术负责人。我尽量不讲空泛的“数据库设计原则”重点放在你先分析什么问题再动哪些地方中间会遇到哪些坑。整个过程按我平时给团队做的性能排查路径来写你能照着操作。1. 性能提升从哪来先看瓶颈到底卡在哪1.1 先别急着调参先回答三个问题接手一个性能问题我最反感上来就改buffer_pool、改连接数、加缓存的做法。参数确实能掩盖一部分问题但通常掩盖不了太久。先回答三个问题慢的到底是单条查询还是一整类操作这个查询过去不慢是数据量涨了之后才慢的还是一直都慢慢查询消耗的是 CPU、IO还是干脆把锁卡住了这三个问题决定你要不要往“SQL优化”这个方向深挖。比如某个报表查询跑20秒但每天只跑一次那优先级可能不高反而是某个每秒钟执行几十次的简单查询单次只要50毫秒累计起来却把数据库拖到CPU 90%。这种问题光看平均耗时是看不出来的必须结合执行频率看。我之前接手过一套系统界面卡顿严重第一反应是服务器负载太高。结果把慢查询日志打开排在最前面的是一条看起来人畜无害的查询每天调用超过百万次每次要扫描近百万行记录取最新一条。问题不在服务器也不在网络就是这条SQL把一条本来可以走索引的路径写成了让优化器只能全表扫的形态。1.2 建立性能基线没有数据你就没有发言权很多人排查问题靠感觉这是大忌。你至少要采集以下几类数据再去定位问题慢查询日志里所有超过1秒的SQL按平均耗时×执行次数排序数据库当前的活跃会话数是否长时间堆积磁盘读写延迟和吞吐是否存在明显排队缓存命中率、临时表创建频率、扫描行数这些数据除了说明现状更重要的作用是建立“优化前的基线”。30天计划里第1天就该把这些数据记下来。后面每次改动只验证一个指标对照基线看是否变好。没有基线的优化很容易陷入“改完感觉快了点”的玄学状态。2. 第一轮体检用十条核心规则把“烂 SQL”筛出来2.1 哪些写法一看就知道要出问题我总结过一套快速筛查清单不需要很高深的功底照着对照就能发现大部分问题SELECT *尤其出现在大宽表、多表关联里。它会把不需要的字段全捞出来增加IO和网络传输。真实业务里大多数查询只需要其中几个字段。在索引列上做函数运算WHERE YEAR(create_time) 2024、WHERE DATE(create_time) CURDATE()优化器基本无法使用该列的索引。正确写法是改成范围条件。隐式类型转换WHERE user_id 123里如果user_id是整型字符串常量会导致索引失效。反过来字段是字符串、参数传数字也一样。前导通配符LIKE %关键词%尤其是%开头多数数据库无法走索引只能全表扫。大表连大表两张百万级以上数据量的表以临时表方式关联如果把过滤条件放在关联之后做扫描行数会爆炸成笛卡尔积的规模。分页深度过深LIMIT 100000, 20不是先找到20条而是先扫描100020条再丢弃。数据量一大这几乎是灾难。这些是第一批要处理的对象。其中任何一个单独出现在低并发场景可能还好一旦并发上来数据库的CPU和磁盘IO会成倍放大这种性能损耗。逐个修正时手法不一定一样但常见的改善方向是减少扫描行数、减少回表次数、减少无需的排序和临时表。2.2 索引不是越多越好而是要“对症下药”很多团队的索引设计基本属于“谁报错就加谁”。结果是表上挂了十几个索引写入变慢优化器反而容易选错。我的做法是反着来先看慢查询日志里高频查询的WHERE条件再设计联合索引。联合索引有个重要规律叫“最左前缀”但优先级顺序要按实际情况排。把区分度高的字段放前面把范围条件字段放后面。比如WHERE status ? AND category_id ? ORDER BY create_time DESC联合索引可以考虑(status, category_id, create_time)让排序也能借助索引完成而不是额外走一次文件排序。这里有个容易忽略的坑有些字段区分度极低比如订单状态只有“待支付、已支付、已取消”几种把它放在联合索引最前面过滤效果有限。但它在某些场景下依然有必要放前面因为它可能被用来避免大量无关行的回表。这就要结合具体数据分布和查询模式来判断不能套死公式。3. 实操从一个具体场景看优化全过程3.1 模拟慢查询的原貌假设系统里有一条查询“获取待发货订单列表”慢查询日志显示单次执行要 2.8 秒每分钟调用约 120 次。原SQL长这样SELECT * FROM orders o LEFT JOIN order_items oi ON o.id oi.order_id WHERE o.status ! completed AND o.created_at 2023-01-01 ORDER BY o.created_at DESC LIMIT 20;这里有几个致命问题status ! completed是不等值条件优化器很难有效利用索引等于把大部分订单都扫描一遍。SELECT *把订单主表和子表所有字段都拽出来造成大量无谓传输。LEFT JOIN加上订单子表的关联让优化器至少要把满足条件的订单找出来再去关联明细而明细表通常更大。ORDER BY created_at DESC在广范围扫描后做排序又增加一次临时文件排序。数据量一旦从早期的几万订单涨到几百万订单这个查询自然从几十毫秒劣化到几秒。这就是“业务量涨了之后性能崩了”的典型路径。3.2 逐项改造并验证每一步效果改造并不是一次性重写那样出了问题不好定位。我习惯一次只改一个维度立刻看执行计划和执行时间。第一步先干掉SELECT *只保留真正要用的字段。这一步也许只减少几十毫秒但能让后续的覆盖索引建起来有效果。第二步把status ! completed改成正向枚举。如果业务上只有三种状态可以直接写status IN (pending, shipped)。不信等值条件优化器至少能初步锁定一个更小的集合。第三步确认关联是否真的需要LEFT JOIN。如果只是展示订单及其商品名称完全可以在订单表查出20条后再用WHERE order_id IN (...)查明细而不是一次性把所有明细也关联进来。这个改动往往收益最大。第四步针对排序条件设计联合索引。假设最终高频过滤条件是status和created_at可以考虑ALTER TABLE orders ADD INDEX idx_status_created (status, created_at DESC);再把查询改成SELECT o.id, o.order_no, o.status, o.created_at, o.customer_name FROM orders o WHERE o.status IN (pending, shipped) AND o.created_at 2023-01-01 ORDER BY o.created_at DESC LIMIT 20;之后需要用EXPLAIN看执行计划确认type从ALL变成了range或refExtra里不再出现Using filesort。实测下来同样的业务条件这条查询可以从原来的 2.8 秒降到 150 毫秒以内而且并发上去后波动也小很多。3.3 为什么“倒序索引”和“覆盖索引”容易被忽略很多数据库默认索引升序但业务排序经常是倒序。以前常见做法是让数据库排序数据量大就慢。现在主流数据库都支持倒序索引直接在创建索引时指定DESC可以让索引顺序和查询排序一致省掉排序环节。覆盖索引是另一个被低估的东西。简单理解就是索引本身已经把查询需要的字段都包含了数据库扫完索引就能拿到全部结果不需要回原表再查一次。这在高并发场景里的收益非常明显尤其当表行宽很大、字段很多时。我的建议是高频接口只查固定几个字段就为这几个字段建立覆盖索引不要为了覆盖全部查询把所有字段塞进索引那会白白增加写入开销。4. 常见问题与排查技巧实录4.1 优化完没效果大概率是执行计划没走新索引这是最常遇到的情况。你以为建了索引就会用但优化器判断下来可能认为你的查询本身扫描范围还是太大走索引的成本不如全表扫。常见原因有三个数据量还不够大全表扫的代价实际很低优化器选择不走索引。这种情况下不用太纠结量变到一定程度自然就走索引了。统计信息过期。数据库优化器需要靠表的统计信息来估算行数如果统计信息一直没更新它就会按过期的数据量来决策。我的习惯是表数据变化超过20%时手动触发一次统计信息更新。查询条件里存在函数或隐式转换索引无法生效。检查一下字段类型和常量类型是否匹配必要时把函数运算移到应用层。遇到这种问题别去强制指定索引治标不治本。先用EXPLAIN看看优化器估算的行数再对照实际行数基本就能判断是统计信息问题还是索引设计问题。强制索引最多算临时避险手段后续还是要从写法和结构上解决。4.2 临时表和文件排序的隐形夹击我见过不少看似优化完的SQL跑起来数据量不大但EXPLAIN的Extra列里一堆Using temporary; Using filesort。这两兄弟是性能杀手。临时表一旦落到磁盘就是大量随机IO文件排序数据量一大同样会拖垮整个实例。排查技巧是把所有高频SQL的EXPLAIN结果收集起来凡是出现Using temporary或Using filesort的优先处理。处理方向一般是去掉不必要的DISTINCT、ORDER BY尤其是它们和GROUP BY、关联一起出现时。让排序字段和索引顺序一致避免额外的排序步骤。如果GROUP BY不可避免尝试重写成窗口函数。数据量大时窗口函数往往能避免一次大临时表。调大tmp_table_size和max_heap_table_size有一定效果但这只是把磁盘临时表变成内存临时表属于缓解不是根治。4.3 更新和删除也会拖慢查询锁与膨胀很多人只盯着SELECT慢忽略了更新和删除操作对查询的影响。在大表上直接UPDATE某一行可能触发行锁、间隙锁或者让表产生大量碎片。尤其高频删改后表和索引的膨胀会把原本挺好的查询计划重新拖慢。这里有一个别人很少提的坑明明逻辑上只是逻辑删除却反复执行物理删除导致索引页分裂严重。我比较推荐在数据量可控的前提下用“状态位定时归档任务”替代高频物理删除。真的非删不可就分批删除避免一次锁定大量数据。4.4 常见问题速查表现象排查方向常用解法同一个查询越跑越慢看表数据量、统计信息、索引碎片更新统计信息整理碎片重建索引并发一高就CPU飙升抓慢查询日志关注高频低效SQL重写SQL、加覆盖索引、减少全表扫描关联查询极慢看驱动表和被驱动表行数、连接字段索引小表驱动大表连接字段建索引避免大范围扫描ORDER BY性能差看Extra是否出现filesort联合索引包含排序列或改倒序索引数据量不大但很慢检查是否有隐式转换、函数索引失效统一字段类型改写查询条件偶尔慢平时正常看锁等待、备份任务、定时批量任务冲突错峰执行检查事务隔离级别下的锁范围这张表可以打印出来遇到一个处理一个处理完就更新一次基线数据。很多团队就是靠这个循环把优化变成“看得见进度”的30天计划。5. 工具选型与30天节奏安排5.1 工具不在多够用就行市面上有很多专业监控工具但如果你只是需要快速定位SQL问题优先级应该是这样的数据库自带的慢查询日志。它是最直接、最没有依赖的入口。先把阈值设到1秒抓一天的日志按“平均耗时×次数”排序头20条基本就是你要优化的对象。自带的执行计划工具比如EXPLAIN。不需要看太深抓住扫描行数、访问类型、是否排序、是否临时表这几个关键列就行。一个简单的可视化监控能看CPU、磁盘IO、活跃连接数趋势。别一上来就追求大而全的监控平台太多指标会把注意力分散掉。选工具的核心原则是“能解决问题的就是好工具”。我见过有人花了好几周搭监控平台最后优化还没有真正开始。优先把已有的能力用透再谈引入复杂度。5.2 30天计划怎么拆标题说的“30天”我认为不是真的需要每天从早改到晚而是给你一个合理的节奏把“发现问题—分析问题—改造—验证”走通两轮。我建议这样排第1周采集慢查询日志和基线指标梳理TOP SQL建立数据库性能台账。输出一份“必须优化清单”。第2周集中处理清单里的前20个高频慢SQL逐条EXPLAIN、改写法、补索引。每次改动后跑回归确认无副作用。第3周处理结构性问题。把大表拆成归档表、加分区、调整事务范围、清理无用索引。这一周通常能让效果进一步放大。第4周做一次全量压测或并发回归对比30天前的基线数据整理优化报告。把可复用的检查规则沉淀进团队的开发规范里。这个计划的关键不是每个SQL都优化到极致而是每周都能看到明确产物。到了第30天你拿得出手的不只是一堆“改了哪些SQL”而是一套未来新代码如何避免踩坑的机制。比如对慢查询日志做周度巡检、在发布流程里卡住没有索引的新SQL。5.3 关于“终极秘诀”的客观说法真正做了大量优化后你会发现所谓“终极秘诀”更像是一套习惯先定位最痛的查询再拿执行计划分析然后用最小代价修改最后用数据验证。它不神秘也不复杂只是一旦成为团队约定成绩会非常稳定。我在多个系统上实测类似流程最终总能发现性能崩掉的根源不是某个高深参数而是几十条普通SQL叠加出来的浪费。把这些浪费一点点挤掉就足以让整体性能发生数量级变化。如果你连一周都坚持不了那任何“秘诀”都救不了你反过来只要严格执行30天后的效果通常比你预期的更好。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →