• 如何优化一条SQL语句的性能?

如何优化一条SQL语句的性能?

2025-05-12 10:00:03 栏目:宝塔面板 54 阅读

前言

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

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

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

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

希望对你会有所帮助。

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.手术方案:精准打击

(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时临时表生成

(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表达式谨慎使用

(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.术后护理:体系化治理

(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分析流水线

(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/200.html

搜索文章

Tags

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