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

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

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

前言

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

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