SQL优化思路

SQL查询与建表优化技巧
本文介绍了一系列SQL查询优化方法,包括避免全表扫描、合理使用通配符、替换OR为UNION ALL等;同时提供了建表时的优化建议,如选择合适的字段类型和减少字段长度等。
查询优化:避免全表扫描
  1. 避免通配符紧随like出现,如LIKE ‘%ABC’
  2. 避免在WHERE子句中使用IS NULL
  3. 避免使用不等于:!= , <>
  4. 避免使用OR, 尽量使用UNION ALL
  5. 尽量用EXIST子查询代替IN子程序
  6. 避免只给出复合索引中的其中一个字段
  7. 避免WHERE 1=1这样的写法
  8. 避免在WHERE子句中对字段进行表达式操作,如WHERE num/2=100
  9. 避免在WHERE子句中对字段进行函数操作,如SUBSTR(name,1,2)=‘ab’
建表优化
  1. 尽可能使用数字型字段
  2. 尽量使用varchar代替char,节省存储空间
其他优化
  1. 除非查询所有字段,否则应避免使用SELECT *
SQL 优化是数据库性能调优的核心环节。一个高效的 SQL 不仅能提升响应速度,还能降低服务器负载、减少锁竞争和资源消耗。 以下是 **系统化、可落地的慢 SQL 优化思路**,从“发现问题”到“彻底解决”,并附上代码示例与执行命令。 --- ## ✅ 一、慢 SQL 优化的整体思路(6 步法) ```text 1. 发现 → 2. 定位 → 3. 分析 → 4. 优化 → 5. 验证 → 6. 预防 ``` 我们逐层展开: --- ## ✅ 第一步:发现慢 SQL(如何知道有慢查询?) ### 🔹 方法 1:开启 MySQL 慢查询日志 ```ini # my.cnf 配置 [mysqld] slow_query_log = ON slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 # 超过1秒记录 log_queries_not_using_indexes = ON # 记录未用索引的SQL ``` 动态启用(无需重启): ```sql SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; ``` ### 🔹 方法 2:使用 `pt-query-digest` 分析日志(推荐工具) ```bash pt-query-digest /var/log/mysql/slow.log > report.txt ``` 输出内容包括: - 最耗时 Top 10 SQL - 执行次数最多但总时间长的 SQL - 是否存在全表扫描、临时表、文件排序等问题 > 📌 推荐每天定时生成报告,自动告警异常 SQL --- ## ✅ 第二步:定位问题 SQL(哪条 SQL 最需要优化?) 通过 `pt-query-digest` 报告找到以下特征的 SQL: | 特征 | 说明 | |------|------| | `Query_time` 平均 > 1s | 用户已明显感知卡顿 | | `Rows_examined` > 1万 | 扫描行数过多,可能缺索引 | | `Executes` 次数高 × 单次慢 | 总耗时巨大,优先级最高 | 👉 示例: ```text # Query 1: 1.2k QPS, Avg time 1.8s, Examines 100K rows select * from orders where user_id=123 and status='PAID' ``` → 这类高频+高延迟 SQL 是首要优化目标! --- ## ✅ 第三步:分析执行计划(为什么慢?) 使用 `EXPLAIN` 查看 SQL 执行路径: ```sql EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID'; ``` 重点关注字段: | 字段 | 合理值 | 异常表现 | 含义 | |------|--------|----------|-------| | `type` | `ref`, `range`, `index` | `ALL` ❌ | 访问类型,`ALL`=全表扫描 | | `key` | 非 NULL | `NULL` ❌ | 实际使用的索引 | | `rows` | 越小越好 | >10000 ❌ | 预估扫描行数 | | `Extra` | — | `Using filesort`, `Using temporary` ❌ | 额外操作,影响性能 | --- ## ✅ 第四步:常见问题及优化策略 ### 🟢 优化策略 1:添加合适索引 #### 场景:WHERE 条件字段无索引 ❌ 慢: ```sql SELECT * FROM orders WHERE user_id = 123; -- type=ALL, key=NULL → 全表扫描 ``` ✅ 快: ```sql ALTER TABLE orders ADD INDEX idx_user_id (user_id); ``` #### 复合条件 → 建复合索引 ```sql SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID'; ``` ✅ 创建联合索引: ```sql ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); ``` > ⚠️ 注意最左前缀原则:`WHERE status='PAID' AND user_id=123` 也能命中该索引 --- ### 🟢 优化策略 2:使用覆盖索引减少回表 当查询字段都在索引中时,无需回到主键查数据。 ❌ 回表多: ```sql -- 查询了 create_time,但索引只有 (user_id, status) SELECT create_time FROM orders WHERE user_id = 123; ``` ✅ 改为覆盖索引: ```sql ALTER TABLE orders ADD INDEX idx_covering (user_id, status, create_time); ``` 此时 `Extra=Using index` → 直接从索引获取数据,不回表! --- ### 🟢 优化策略 3:避免 `Using filesort`(文件排序) #### 问题:ORDER BY 字段无索引 ❌ 慢: ```sql SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC; -- Extra: Using filesort ``` ✅ 解决:创建 `(user_id, create_time)` 联合索引 ```sql ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time); ``` 这样既能快速过滤用户订单,又能按时间排序,避免额外排序开销。 --- ### 🟢 优化策略 4:深分页优化(LIMIT M,N) #### 问题:跳过大量行 ❌ 慢: ```sql SELECT * FROM orders ORDER BY id LIMIT 10000, 20; -- 跳过1万行,效率极低 ``` ✅ 方案 1:基于主键继续查询(推荐) ```sql -- 上一页最后一条记录 id=10000 SELECT * FROM orders WHERE id > 10000 ORDER BY id LIMIT 20; ``` ✅ 方案 2:延迟关联(适用于非主键排序) ```sql SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY create_time LIMIT 10000, 20 ) t ON o.id = t.id; ``` 只对 ID 排序分页,再关联原表,大幅减少扫描。 --- ### 🟢 优化策略 5:避免函数操作字段(导致索引失效) ❌ 慢: ```sql SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01'; -- 函数导致索引无法使用 ``` ✅ 快: ```sql SELECT * FROM orders WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00'; ``` 让字段独立出现在左边,才能走索引。 --- ### 🟢 优化策略 6:禁止 `SELECT *` ❌ 浪费带宽、增加回表概率、影响覆盖索引命中 ```sql SELECT * FROM large_table WHERE condition = 'xxx'; ``` ✅ 明确指定字段: ```sql SELECT id, name, email FROM users WHERE active = 1; ``` 尤其在有索引的情况下,可以更好地利用覆盖索引。 --- ### 🟢 优化策略 7:防止 N+1 查询(应用层问题) ❌ Java 示例: ```java List<User> users = userDao.findAll(); // 1次 for (User u : users) { List<Order> orders = orderDao.findByUserId(u.getId()); // N次 } ``` ✅ 改为批量查询: ```sql SELECT * FROM orders WHERE user_id IN (1,2,3,...); ``` 或使用 JOIN 一次性查出所有关联数据。 --- ## ✅ 第五步:验证优化效果 优化后必须验证是否真正变快! ### 方法 1:对比执行时间 ```sql -- 优化前 SELECT SQL_NO_CACHE * FROM orders WHERE user_id = 123; -- 添加索引后 ALTER TABLE orders ADD INDEX idx_user_id (user_id); -- 再次执行,观察时间变化 SELECT SQL_NO_CACHE * FROM orders WHERE user_id = 123; ``` > 使用 `SQL_NO_CACHE` 避免查询缓存干扰测试结果 ### 方法 2:再次运行 `EXPLAIN` 确认: - `type` 从 `ALL` 变成 `ref` - `rows` 从 10万 → 100 - `Extra` 不再出现 `Using filesort` --- ## ✅ 第六步:建立预防机制(防患于未然) | 措施 | 说明 | |------|------| | ✅ 开发规范 | 禁止 `SELECT *`、强制审核上线 SQL | | ✅ APM 监控 | 使用 Druid、SkyWalking 实时监控 SQL 耗时 | | ✅ 慢 SQL 告警 | 设置阈值(如 >500ms)自动通知 | | ✅ 定期巡检 | 每周分析慢日志,持续优化 | | ✅ 使用 ORM 注意事项 | MyBatis/Hibernate 避免自动生成低效 SQL | --- ## ✅ 附加技巧:SQL 重写建议 | 原写法 | 优化写法 | 原因 | |--------|-----------|--------| | `LIKE '%abc%'` | 尽量不用,或用全文索引 | 前导通配符无法用索引 | | `IN (子查询)` | 改为 `JOIN` 或缓存结果 | 子查询可能不走索引 | | `OR` 条件 | 改为 `UNION` | OR 容易导致索引失效 | | `NOT IN` / `!=` | 改为 `LEFT JOIN IS NULL` | 否定条件难优化 | --- ## ✅ 总结:慢 SQL 优化 Checklist ✅ 每次遇到慢 SQL,请按此清单排查: | 检查项 | 是否完成 | |--------|----------| | ☐ 是否开启了慢查询日志? | □ | | ☐ 是否用 `pt-query-digest` 分析了 Top 慢 SQL? | □ | | ☐ 是否使用 `EXPLAIN` 查看了执行计划? | □ | | ☐ `type` 是否为 `ALL`?需加索引 | □ | | ☐ `Extra` 是否有 `Using filesort` 或 `Using temporary`? | □ | | ☐ 是否存在深分页 `LIMIT 10000,20`? | □ | | ☐ 是否用了 `SELECT *`? | □ | | ☐ WHERE 中是否有函数操作字段? | □ | | ☐ 是否存在 N+1 查询? | □ | | ☐ 优化后是否验证了效果? | □ | > 💡 **黄金法则:不要猜!要用工具看真实数据** --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值