• 如何“医治”一条慢SQL?

如何“医治”一条慢SQL?

2025-05-20 00:00:03 栏目:宝塔面板 59 阅读

前言

"苏工,订单列表又崩了!"

接到电话时,我对着监控大屏上999ms的SQL响应时间哭笑不得。

几年来,我发现一个定律:所有SQL问题都是在凌晨三点爆发!

今天抽丝剥茧,教你用架构师的思维给慢SQL开刀手术。

希望对你会有所帮助。

1.术前检查:找准病灶

1.1 EXPLAIN 查看执行计划

使用EXPLAIN查看SQL语句的执行计划,相当于给SQL拍了张X光。

下面是一个典型的SQL问题,它是某电商平台历史订单查询的SQL语句:

SELECT * 
FROM orders o 
LEFTJOINusers u ON o.user_id = u.id
LEFTJOIN products p ON o.product_id = p.id
WHERE o.create_time > '2023-01-01'
AND u.vip_level > 3
AND p.category_id IN (5,8)
ORDERBY o.amount DESC
LIMIT1000,20;

使用EXPLAIN关键字查看执行计划的结果如下:

+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+
| id | select_type | table | type | possible_keys | key  | rows    | Extra| key_len | 
+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+
| 1  | SIMPLE      | o     | ALL  | idx_user_time | NULL | 1987400 | Using where; Using filesort     |  
| 1  | SIMPLE      | u     | ALL  | PRIMARY       | NULL | 100000  | Using where                     |
| 1  | SIMPLE      | p     | ALL  | PRIMARY       | NULL | 50000   | Using where                     |
+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+

诊断报告

  • 全表扫描三连击(type=ALL)
  • filesort暴力排序(内存警告)
  • 索引全军覆没

2.手术方案:精准打击

2.1 单表代谢手术

如果通过执行计划查到是索引有问题,我们就需要单独优化索引。

病根:JSON字段索引失效

错误用法:

ALTER TABLE users ADD INDEX idx_extend ((extend_info->'$.is_vip'));

extend_info字段是JSON类型的字段,即使创建了索引,索引也会丢失。

正解姿势(MySQL 8.0+):

ALTER TABLE users ADD INDEX idx_vip_level (vip_level);
ALTER TABLE orders ADD INDEX idx_create_user (create_time, user_id) COMMENT '组合索引覆盖查询';

创建组合索引覆盖查询。

2.2 血管疏通术

卡点分析

原始join顺序是:

orders → users → products

优化后的方案:

(子查询过滤users) → products → orders

调整执行顺序,用小表驱动大表。

重写后的SQL:

SELECT o.* 
FROM products p 
INNERJOIN (
SELECT o.id, o.amount, o.create_time 
FROM orders o 
WHERE o.create_time > '2023-01-01'
) o ON p.id = o.product_id 
INNERJOIN (
SELECTid
FROMusers
WHERE vip_level > 3
) u ON o.user_id = u.id  
WHERE p.category_id IN (5,8)
ORDERBY o.amount DESC
LIMIT1000,20;

术后效果

  • 先扫小表(users过滤后只有100条)
  • 消除冗余字段传输
  • 减少Join时临时表生成

2.3 开颅手术

通过执行计划锁定了问题,走错索引了,该怎么处理呢?

可以通过FORCE INDEX强制指定索引:

SELECT /*+ INDEX(o idx_create_user) */ 
       o.id, o.amount 
FROM orders o FORCE INDEX (idx_create_user)
WHERE o.create_time > '2023-01-01';

使用衍生表加速:

SELECT *
FROM (
SELECTid, amount 
FROM orders 
WHERE create_time > '2023-01-01'
ORDERBY amount DESC
LIMIT1020
) tmp 
ORDERBY amount DESC
LIMIT1000,20;

医嘱

  • 警惕OR导致的索引失效
  • 用覆盖索引避免回表查询
  • CTE表达式谨慎使用

2.4 生命体征监测

查看索引使用:

SHOW INDEX FROM orders;

监控索引使用率:

SELECT object_schema, object_name, index_name,
       count_read, count_fetch 
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL;

3.术后护理:体系化治理

3.1 SQL消毒中心

需要制定优秀的代码规范,否则可能会出现全表扫描的问题。

在日常工作中,我们要尽可能减少Java代码感染源

MyBatis危险写法:

@Select("SELECT * FROM orders WHERE #{condition}")
List findByCondition(@Param("condition") String condition);

condition参数可以传入任何内容,如何传入了1=1,可能会导致查询所有的数据,走全表扫描,让查询效率变得非常低。

正确做法(参数化查询):

@Select("SELECT * FROM orders WHERE create_time > #{time}")
List findByTime(@Param("time") Date time);

消毒方案

  • SQL审核平台接入(如Yearning)
  • MyBatis拦截器拦截全表更新
  • 自动化EXPLAIN分析流水线

3.2 查杀大表癌症

如果遇到大表的癌症病例,可以用分库分表的方案解决。

病历案例:3亿订单表终极解决方案

// Sharding-JDBC分片配置
spring.shardingsphere.rules.sharding.tables.orders.actual-data-nodes=ds$0..1.orders_$->{2020..2023}
spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-column=create_time
spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-algorithm-name=time_range

化疗方案

  • 时间维度分片(2020~2023年度表)
  • 用户ID取模分库
  • 冷热分离(OSS归档历史数据)

医嘱总结

优化三板斧

  • 定位:慢查询日志+执行计划分析
  • 切割:化繁为简拆分多步执行
  • 重建:符合业务场景的数据结构

避坑口诀

  • 索引不是银弹,覆盖才是王道
  • Join水深,能拆就拆
  • Order By+Limit≠分页优化

最后送上苏三的传秘方:当你优化SQL到怀疑人生时,不妨试试这三味药:

  • 删业务逻辑
  • 加缓存
  • 换数据库

保证药到病除(老板打不打死你我就不管了,哈哈哈)!

本文地址:https://www.yitenyun.com/218.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL Web 应用 异步数据库 数据同步 ACK 双主架构 循环复制 TIME_WAIT 运维 负载均衡 Deepseek 宝塔面板 Linux宝塔 Docker JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 生命周期 序列 核心机制 SSL 堡垒机 跳板机 HTTPS HexHub Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 HTTPS加密 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 Oracle 处理机制 InnoDB 数据库锁 连接控制 机制 无法访问宝塔面板 ES 协同 监控 Windows宝塔 Mysql重置密码 Serverless 无服务器 语言 开源 PostgreSQL 存储引擎 技术 group by 索引 Spring Redis 异步化 分页查询 服务器 管理口 高可用 缓存方案 缓存架构 缓存穿透 SQL 动态查询 响应模型 SVM Embedding 自定义序列化 日志文件 MIXED 3 GreatSQL 连接数 数据 主库 服务器性能 SQLark 云原生 PG DBA scp Linux的scp怎么用 scp上传 scp下载 scp命令 ​Redis 机器学习 推荐模型 R edis 线程 Netstat Linux 服务器 端口 AI 助手 向量数据库 大模型 工具 Linux 安全 共享锁 存储 Undo Log 查询 SQLite-Web SQLite 数据库管理工具 openHalo OB 单机版 电商 系统 Recursive 架构 Rsync R2DBC Postgres OTel Iceberg • 索引 • 数据库 RocketMQ 长轮询 配置 聚簇 非聚簇 数据分类 加密 修改DNS Centos7如何修改DNS 同城 双活 sftp 服务器 参数 优化 万能公式 redo log 重做日志 磁盘架构 流量 信息化 智能运维 防火墙 黑客 Ftp Hash 字段 场景 MySQL 9.3 mini-redis INCR指令 RDB AOF MVCC 人工智能 推荐系统 数据备份 窗口 函数 缓存 高效统计 今天这篇文章就跟大家 业务 网络架构 网络配置 INSERT COMPACT Redisson 锁芯 向量库 Milvus Doris SeaTunnel 线上 库存 预扣 事务 Java 开发 Python 核心架构 订阅机制 prometheus Alert 引擎 性能 PostGIS B+Tree ID 字段 崖山 新版本 MongoDB 数据结构 IT运维 不宕机 Web 数据类型 数据脱敏 加密算法 传统数据库 向量化 分布式 集中式 虚拟服务器 虚拟机 内存 ZODB 读写 网络故障 容器 容器化 DBMS 管理系统 模型 OAuth2 Token filelock JOIN Canal QPS 高并发 Redis 8.0 Pottery 微软 SQL Server AI功能 自动重启 部署 聚簇索引 非聚簇索引 Testcloud 云端自动化 发件箱模式 SpringAI 锁机制 分页方案 排版 Entity 排行榜 排序 分库 分表 事务隔离 启动故障 数据页 工具链 速度 服务器中毒 Web 接口 1 悲观锁 乐观锁 StarRocks 数据仓库 SSH Caffeine CP sqlmock LRU 池化技术 连接池 单点故障 Go 数据库迁移 MCP 开放协议 频繁 Codis 原子性 大表 业务场景 数据集成工具 Redka 分页 意向锁 记录锁 AIOPS 分布式架构 分布式锁​ Order 优化器 网络 dbt 数据转换工具 日志 EasyExcel MySQL8 IT 仪表盘 字典 对象 单线程 InfluxDB 双引擎 RAG HelixDB 行业 趋势 Ansible 事务同步 Crash 代码 国产数据库 LLM UUIDv7 主键 订单 线程安全 List 类型 Pump UUID ID 主从复制 代理 Next-Key 编程 关系数据库 Valkey Valkey8.0 播客 解锁 调优 ReadView 产业链 兼容性 语句 恢复数据 MGR 分布式集群 数据字典 失效 算法 国产 用户 矢量存储 数据库类型 AI代理 查询规划 千万级 快照读 当前读 视图 GitHub Git 慢SQL优化 Weaviate RR 互联网 count(*) count(主键) 行数 神经系统 表空间 分布式锁 Zookeeper 拦截器 动态代理 并发控制 恢复机制 CAS 多线程 技巧 闪回