当前位置: 首页 > news >正文

SQL性能优化实战:从慢查询到秒开(详细代码注释)

前言

你写的SQL跑了30秒,老板催你,客户等着。

然后你把索引加上,1秒搞定。

这不是玄学,是有方法论的。

本文覆盖SQL性能优化最核心的5个方向:

✅ 读懂EXPLAIN执行计划

✅ 索引的正确姿势(和常见误区)

✅ 查询改写技巧

✅ 大表优化策略

✅ 千万级数据实战案例

💡 文末有关注船长Talk公众号的方式,每周更新数据分析实战教程,不定期分享面试真题。

一、读懂EXPLAIN:优化的起点

优化SQL的第一步:先看执行计划,不要盲目加索引。

1.1 基本用法

-- 在任意SELECT前加EXPLAIN EXPLAIN SELECT * FROM orders WHERE user_id = 1001; -- 更详细的信息(MySQL 8.0+) EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1001;

1.2 关键字段解读

EXPLAIN输出的关键列:

-- EXPLAIN输出示例 +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ | 1 | SIMPLE | orders | NULL | ALL | NULL | NULL | NULL | NULL | 998234 | 10.00 | Using where | +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

最重要的字段:type(访问类型)

type值从最优到最差排列: const → 最优:通过主键/唯一索引查1条记录 eq_ref → 很优:join时每个组合最多1条 ref → 好:非唯一索引等值查询 range → 一般:索引范围扫描(BETWEEN/>/< ) index → 一般:全索引扫描 ALL → 最差:全表扫描(优化目标:避免ALL)
-- 实战:看到ALL就要警惕 EXPLAIN SELECT * FROM orders WHERE create_time > '2026-01-01'; -- type: ALL → 说明create_time没有索引,需要添加 -- 加索引后 ALTER TABLE orders ADD INDEX idx_create_time (create_time); -- 再次EXPLAIN EXPLAIN SELECT * FROM orders WHERE create_time > '2026-01-01'; -- type: range → 从全表扫描变为范围索引扫描,速度提升10x~100x

1.3 rows字段:最直观的性能指标

-- rows表示MySQL预估要扫描的行数 -- 1000万行数据的表,rows=1000000说明没用上索引 -- 慢查询示例 EXPLAIN SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid'; -- rows: 998234 → 扫了100万行,说明user_id或status没有索引 -- 加联合索引 ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); -- 再次EXPLAIN EXPLAIN SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid'; -- rows: 12 → 只扫12行,性能提升约8万倍

二、索引设计:加对才有效

2.1 索引基本原则

-- ✅ 应该加索引的情况 -- 1. WHERE条件字段 SELECT * FROM orders WHERE user_id = ?; -- → ALTER TABLE orders ADD INDEX idx_user_id (user_id); -- 2. JOIN关联字段 SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id; -- → users.id通常是主键已有索引 -- → orders.user_id需要加索引 -- 3. ORDER BY字段(避免filesort) SELECT * FROM orders ORDER BY create_time DESC LIMIT 10; -- → ALTER TABLE orders ADD INDEX idx_create_time (create_time); -- 4. GROUP BY字段 SELECT user_id, COUNT(*) FROM orders GROUP BY user_id; -- → ALTER TABLE orders ADD INDEX idx_user_id (user_id);

2.2 联合索引:最左前缀原则

-- 建立联合索引(顺序很重要) ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time); -- ✅ 能用上索引的查询(满足最左前缀) SELECT * FROM orders WHERE user_id = 1001; -- 用上了idx_user_status_time的第一列 SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid'; -- 用上了前两列 SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid' AND create_time > '2026-01-01'; -- 用上了全部三列 -- ❌ 用不上索引的查询(跳过了第一列) SELECT * FROM orders WHERE status = 'paid'; -- 跳过user_id,无法使用联合索引 SELECT * FROM orders WHERE create_time > '2026-01-01'; -- 跳过user_id和status,无法使用联合索引 -- 🔑 口诀:联合索引使用时,不能跳过中间的列

2.3 索引失效:这些写法会让索引白加

-- ❌ 索引失效场景(务必避免) -- 场景1:对索引列做函数操作 SELECT * FROM orders WHERE YEAR(create_time) = 2026; -- 解决方案: SELECT * FROM orders WHERE create_time BETWEEN '2026-01-01' AND '2026-12-31'; -- 场景2:对索引列做运算 SELECT * FROM orders WHERE user_id + 1 = 1002; -- 解决方案: SELECT * FROM orders WHERE user_id = 1001; -- 场景3:LIKE前缀通配符 SELECT * FROM users WHERE name LIKE '%张%'; -- 解决方案(如需模糊查询建议用全文索引或ES): SELECT * FROM users WHERE name LIKE '张%'; -- 只有后缀通配符能用索引 -- 场景4:隐式类型转换 -- phone字段是varchar,但传了数字 SELECT * FROM users WHERE phone = 13800138000; -- 解决方案: SELECT * FROM users WHERE phone = '13800138000'; -- 加引号 -- 场景5:OR条件(某个字段无索引) SELECT * FROM orders WHERE user_id = 1001 OR amount > 1000; -- user_id有索引,amount没有 → 整个条件走全表 -- 解决方案:给amount也加索引,或改用UNION SELECT * FROM orders WHERE user_id = 1001 UNION SELECT * FROM orders WHERE amount > 1000;

三、查询改写:换个写法,快10倍

3.1 用EXISTS替代IN(大数据量时)

-- 问题场景:查找有订单的用户 -- 方法1:IN(子查询返回大量数据时很慢) SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid'); -- 方法2:EXISTS(通常更快) SELECT * FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid' ); -- 规律: -- 外表小、内表大 → 用EXISTS -- 外表大、内表小 → 用IN -- 都大 → 用JOIN

3.2 避免SELECT *,只取需要的列

-- ❌ 低效写法 SELECT * FROM orders WHERE user_id = 1001; -- ✅ 高效写法(只取需要的列) SELECT id, amount, status, create_time FROM orders WHERE user_id = 1001; -- 原因: -- 1. 减少数据传输量 -- 2. 可能触发"覆盖索引"(只读索引,不回表) -- 覆盖索引示例 -- idx_user_id_amount索引包含(user_id, amount) -- 查询只要这两列,就不需要回表读数据页 SELECT user_id, amount FROM orders WHERE user_id = 1001; -- EXPLAIN Extra字段会显示:Using index → 触发覆盖索引

3.3 深度分页优化

-- 问题:LIMIT偏移量很大时极慢 -- ❌ 低效(扫了10001行,只返回10行) SELECT * FROM orders ORDER BY id LIMIT 100000, 10; -- ✅ 方法1:游标分页(已知上一页最后一条ID) SELECT * FROM orders WHERE id > 99999 -- 上一页最后一条id ORDER BY id LIMIT 10; -- ✅ 方法2:子查询定位ID(先定位,再取数据) SELECT * FROM orders WHERE id >= ( SELECT id FROM orders ORDER BY id LIMIT 100000, 1 ) ORDER BY id LIMIT 10; -- 性能对比(1000万数据): -- LIMIT 100000,10 → 约2.3秒 -- 游标分页 → 约0.01秒(提升200倍)

3.4 用UNION ALL替代UNION(无需去重时)

-- ❌ UNION会排序去重,多一步操作 SELECT user_id FROM orders WHERE status = 'paid' UNION SELECT user_id FROM returns WHERE status = 'approved'; -- ✅ 确定无重复数据时用UNION ALL SELECT user_id FROM orders WHERE status = 'paid' UNION ALL SELECT user_id FROM returns WHERE status = 'approved'; -- UNION ALL比UNION快约30%(无需排序去重)

四、大表优化策略

4.1 分页查询 + 覆盖索引实战

-- 场景:订单表5000万条,按用户查最近订单 -- 建索引:(user_id, create_time DESC) ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time); -- 查询 SELECT id, amount, status, create_time FROM orders WHERE user_id = 1001 ORDER BY create_time DESC LIMIT 20; -- EXPLAIN: type=ref, rows≈15, Extra=Using index → 完美

4.2 批量插入替代逐条插入

-- ❌ 低效:逐条插入(每次都有事务开销) INSERT INTO order_items (order_id, product_id, qty) VALUES (1, 101, 2); INSERT INTO order_items (order_id, product_id, qty) VALUES (1, 102, 1); INSERT INTO order_items (order_id, product_id, qty) VALUES (1, 103, 3); -- ✅ 高效:批量插入(一次事务) INSERT INTO order_items (order_id, product_id, qty) VALUES (1, 101, 2), (1, 102, 1), (1, 103, 3); -- Python批量插入示例 import pymysql conn = pymysql.connect(host='localhost', user='root', password='pwd', db='mydb') cursor = conn.cursor() data = [ (1, 101, 2), (1, 102, 1), (1, 103, 3), # 更多数据... ] # executemany比多次execute快3-10倍 cursor.executemany( "INSERT INTO order_items (order_id, product_id, qty) VALUES (%s, %s, %s)", data ) conn.commit() cursor.close() conn.close()

4.3 千万级数据统计优化

-- 场景:订单表1000万条,每天统计GMV -- ❌ 直接聚合(全表扫描) SELECT DATE(create_time) as date, SUM(amount) as gmv FROM orders WHERE create_time >= '2026-01-01' GROUP BY DATE(create_time); -- 耗时:约45秒 -- ✅ 优化方案1:预聚合表 -- 每天凌晨跑一次,把结果存到汇总表 CREATE TABLE daily_gmv ( stat_date DATE PRIMARY KEY, gmv DECIMAL(15,2), order_count INT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 每日更新脚本 INSERT INTO daily_gmv (stat_date, gmv, order_count) SELECT DATE(create_time), SUM(amount), COUNT(*) FROM orders WHERE create_time >= CURDATE() - INTERVAL 1 DAY AND create_time < CURDATE() GROUP BY DATE(create_time) ON DUPLICATE KEY UPDATE gmv = VALUES(gmv), order_count = VALUES(order_count), updated_at = NOW(); -- 查询直接走汇总表 SELECT stat_date, gmv FROM daily_gmv WHERE stat_date >= '2026-01-01' ORDER BY stat_date; -- 耗时:0.001秒(提升45000倍)

五、综合实战:慢查询从18秒优化到0.03秒

场景描述

一个电商平台,订单表orders约800万条,用户行为表user_events约5000万条。

有一个核心报表查询:找出近30天内下单金额超过1000元且最近有活跃行为的用户。

-- 原始查询(耗时18秒) SELECT DISTINCT u.id, u.name, u.email FROM users u WHERE u.id IN ( SELECT user_id FROM orders WHERE create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND amount > 1000 ) AND u.id IN ( SELECT user_id FROM user_events WHERE event_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) );
-- 优化步骤 -- Step1: EXPLAIN分析 EXPLAIN SELECT DISTINCT u.id, u.name, u.email ...; -- 发现: -- orders.create_time无索引(type: ALL) -- orders.amount无索引(type: ALL) -- user_events.event_time无索引(type: ALL) -- Step2: 加索引 ALTER TABLE orders ADD INDEX idx_time_amount (create_time, amount); ALTER TABLE user_events ADD INDEX idx_user_time (user_id, event_time); -- Step3: 改写查询(IN→JOIN+CTE) WITH active_orders AS ( SELECT DISTINCT user_id FROM orders WHERE create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND amount > 1000 ), recent_active AS ( SELECT DISTINCT user_id FROM user_events WHERE event_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) ) SELECT u.id, u.name, u.email FROM users u INNER JOIN active_orders ao ON u.id = ao.user_id INNER JOIN recent_active ra ON u.id = ra.user_id; -- 优化结果:18秒 → 0.03秒(提升600倍)

优化总结

优化路径: 1. EXPLAIN看执行计划 → 找type=ALL的问题 2. 加索引(WHERE/JOIN/ORDER BY字段)→ type从ALL变range/ref 3. 改写查询(IN→JOIN,SELECT*→具体字段) 4. 大表用预聚合/游标分页 5. 验证:再次EXPLAIN,对比rows数量 耗时对比: 原始查询:18秒 加索引后:2.1秒 改写查询:0.03秒 总提升:600倍

六、核心知识速查表

EXPLAIN type值速查:

const → 主键/唯一索引单行查询 | eq_ref → JOIN唯一匹配 | ref → 普通索引等值查询 | range → 索引范围扫描 | index → 全索引扫描 | ALL → 全表扫描(⚠️需优化)

索引设计原则:

① 区分度高的列优先加索引 ② 联合索引遵循最左前缀 ③ 不对索引列做函数操作 ④ 不用LIKE '%前缀通配符'

常用优化技巧:

① SELECT * → 具体列名 ② LIMIT大偏移 → 游标分页 ③ UNION → UNION ALL ④ 大量IN → JOIN ⑤ 每日聚合 → 预计算汇总表


⭐ 更多SQL实战教程,关注「船长Talk」公众号

📊 每周更新:SQL/Python/数据分析干货,职场真相,投资洞察

📱 微信搜索「船长Talk」或扫描文末二维码关注

系列文章推荐:

👉 SQL零基础入门:10个语句解决80%查询问题

👉 SQL窗口函数完整指南:5大高频场景

👉 SQL多表查询:JOIN的7种用法详解

http://www.jsqmd.com/news/701375/

相关文章:

  • 基于安卓的社区法律服务咨询平台毕业设计
  • 类别不平衡问题:从准确率陷阱到工业解决方案
  • Stable Diffusion提示词优化7大进阶技巧
  • ai4j:面向JDK 8+的Java AI全栈开发套件,统一多模型API与Agent构建
  • 集成学习复杂度与奥卡姆剃刀的现代机器学习实践
  • Agenst框架解析:构建多AI智能体协同系统的核心原理与实践
  • 微博开源分布式工作流引擎 rill-flow 核心架构与生产实践详解
  • 基于安卓的社区闲置物品交换平台毕业设计源码
  • 决策树中序数编码的正确使用与实践
  • 轻量级跨平台GUI框架PUAX:从原理到实战的桌面应用开发指南
  • 802.11g与802.11a无线标准技术对比与演进分析
  • VSCode 2026嵌入式调试适配深度解析(2024Q4固件级兼容性白皮书首发)
  • Python字典怎么合并_update()方法与-运算符(Python3.9+)
  • Java开发者如何用LangChain4j构建RAG应用与智能体
  • 【仅剩最后200份】C++26反射面试压轴题库(含微软/字节/英伟达2024Q2真实考题+编译失败日志逐行溯源)
  • FLUX.1-Krea-Extracted-LoRA入门指南:Streamlit UI响应延迟高时的排查路径
  • AgentsMeetRL:强化学习训练LLM智能体的开源项目全景与实战指南
  • 2026年热门的一站式验厂咨询热选公司推荐 - 品牌宣传支持者
  • 机器学习数据准备:从问题框架到特征工程的实战指南
  • TrollInstallerX架构深度解析:iOS 14-16.6.1内核漏洞利用与持久化安装实现机制
  • 机器学习类别不平衡问题:欠采样方法详解与实践
  • Open-AutoGLM:基于视觉大模型的手机端智能体部署与开发实战
  • Java方法级性能监控利器MyPerf4J:低侵入、高精度的性能剖析实战
  • PHP作用域的庖丁解牛
  • 打卡信奥刷题(3166)用C++实现信奥题 P7865 「EVOI-RD1」无人机航拍
  • 2026Q2单相调压器技术解析:三相隔离变压器/交流稳压器/交流调压器/医用隔离变压器/医疗变压器/医疗设备UPS/选择指南 - 优质品牌商家
  • 海外玩家伪装来源? 怎么用IP归属地识别
  • 5分钟搭建原神私服:KCN-GenshinServer图形化一键启动终极指南
  • 抑郁症 = 焦虑症?
  • 2026西南地区尼龙皮PVC皮带厂家名录及选购参考指南:成都托辊生产厂家、成都输送带厂家、沙石料厂皮带、液压输送机选择指南 - 优质品牌商家