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

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

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

前言

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

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