咳咳,各位观众老爷们,晚上好!我是今晚的主讲人,江湖人称“SQL小霸王”(其实是自己封的)。今天给大家带来的是MySQL编程进阶系列之——SQL优化技巧:如何编写可读性强且执行高效的SQL语句。
咱们的目标是:写出像诗一样优雅,跑得像火箭一样快的SQL!
第一部分:SQL优化的大方向:让MySQL知道你要什么
SQL优化,说白了就是让MySQL的查询优化器更好地理解你的意图,然后选择最佳的执行计划。MySQL查询优化器也不是神仙,你写的SQL语句含糊不清,它也只能猜,猜错了自然效率就低了。所以,咱们要做的就是:
- 明确目标: 你想查什么?
- 提供线索: 如何高效地查到?
1.1 避免SELECT *,只取需要的列
这应该是老生常谈了,但还是有很多人犯这个错误。SELECT *
会读取所有列的数据,即使你只需要其中的几列。
-
坏例子:
SELECT * FROM users WHERE id = 1;
-
好例子:
SELECT id, username, email FROM users WHERE id = 1;
好处:
- 减少IO: 只需要读取需要的列,减少磁盘IO。
- 减少网络带宽: 减少数据传输量。
- 减少内存消耗: 减少服务器端和客户端的内存消耗。
- 索引覆盖: 如果查询只涉及到索引列,可以利用索引覆盖,避免回表查询(后面会讲到)。
1.2 善用WHERE条件,缩小查询范围
WHERE条件是SQL查询的核心,用好WHERE条件可以大大缩小查询范围,减少扫描的行数。
-
坏例子:
SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';
如果
order_date
没有索引,或者索引效果不好,这条语句会扫描全表。 -
好例子: (假设
order_date
有索引)SELECT * FROM orders WHERE order_date >= '2023-01-01' AND order_date <= '2023-12-31';
或者,如果可以确定具体的订单状态,加上状态过滤:
SELECT * FROM orders WHERE order_date >= '2023-01-01' AND order_date <= '2023-12-31' AND status = '已完成';
原则:
- 越精确越好: WHERE条件越精确,查询范围越小。
- 索引优先: 尽量使用索引列作为WHERE条件。
- 避免函数计算: 尽量避免在WHERE条件中使用函数计算,例如
WHERE YEAR(order_date) = 2023
,会导致索引失效。
1.3 避免使用!= 或 <> 或 NOT IN 或 NOT EXISTS
这些操作符通常会导致全表扫描,除非有非常好的理由,否则应该尽量避免使用。
-
坏例子:
SELECT * FROM users WHERE status != 'active';
-
好例子:
SELECT * FROM users WHERE status = 'inactive' OR status IS NULL;
如果
status
只有几种状态,可以列举出所有非’active’的状态。对于
NOT IN
,可以尝试使用LEFT JOIN
代替:-- 查找不在order_items表中的product_id -- NOT IN写法 (效率较低) SELECT id FROM products WHERE id NOT IN (SELECT product_id FROM order_items); -- LEFT JOIN写法 (效率较高) SELECT p.id FROM products p LEFT JOIN order_items oi ON p.id = oi.product_id WHERE oi.product_id IS NULL;
1.4 使用EXISTS代替COUNT(*)
当只需要判断是否存在满足条件的记录时,使用EXISTS
比COUNT(*)
更高效。EXISTS
在找到第一条满足条件的记录后就会停止扫描,而COUNT(*)
会扫描全表。
-
坏例子:
SELECT 1 FROM orders WHERE customer_id = 123 AND order_date >= '2023-01-01' AND order_date <= '2023-12-31'; -- 如果需要判断是否存在满足条件的订单,这并不是最佳方法
-
好例子:
SELECT EXISTS (SELECT 1 FROM orders WHERE customer_id = 123 AND order_date >= '2023-01-01' AND order_date <= '2023-12-31');
第二部分:索引:SQL优化的核武器
索引是提高查询效率最有效的手段之一。没有索引,MySQL就只能老老实实地扫描全表。
2.1 索引的种类
- 普通索引 (INDEX): 最基本的索引,没有任何限制。
- 唯一索引 (UNIQUE INDEX): 索引列的值必须唯一,允许有空值 (NULL)。
- 主键索引 (PRIMARY KEY): 一种特殊的唯一索引,不允许有空值。每个表只能有一个主键索引。
- 全文索引 (FULLTEXT INDEX): 用于全文搜索,适用于MyISAM和InnoDB (MySQL 5.6+)。
- 组合索引 (COMPOSITE INDEX): 多个列组合在一起创建的索引。
2.2 如何选择索引
- 经常用于WHERE条件的列: 这是最常见的索引选择依据。
- 经常用于JOIN的列: JOIN操作需要比较列的值,索引可以加快比较速度。
- 区分度高的列: 区分度越高,索引效果越好。例如,性别 (男/女) 的区分度很低,不适合创建索引。而用户ID的区分度很高,适合创建索引。
- 组合索引的列顺序: 将区分度高的列放在前面,区分度低的列放在后面。
2.3 索引的最佳实践
- 避免过度索引: 索引越多,维护成本越高。每次插入、更新、删除数据,都需要更新索引。
- 定期检查索引使用情况: 使用
EXPLAIN
命令分析SQL语句的执行计划,看看是否使用了索引。 -
注意索引失效的情况:
- 使用函数计算: 例如
WHERE YEAR(order_date) = 2023
- 隐式类型转换: 例如
WHERE phone = 13800000000
(phone是字符串类型) - LIKE ‘%abc%’: 以通配符开头会导致索引失效
- 组合索引不满足最左前缀原则: 例如,组合索引是
(a, b, c)
,查询条件只有b
或c
,索引会失效。
- 使用函数计算: 例如
2.4 EXPLAIN:你的SQL透视镜
EXPLAIN
命令可以显示MySQL如何执行SQL语句。通过分析EXPLAIN
的结果,可以判断是否使用了索引,以及查询效率如何。
-
使用方法:
EXPLAIN SELECT * FROM orders WHERE customer_id = 123 AND order_date >= '2023-01-01';
-
重要字段:
type
: 表示连接类型,从好到坏依次是:system > const > eq_ref > ref > range > index > ALL
。system
: 表只有一行记录。const
: 通过主键或唯一索引访问。eq_ref
: 唯一索引扫描,最多只返回一条结果。ref
: 非唯一索引扫描,返回匹配某个单独值的所有行。range
: 索引范围扫描。index
: 全索引扫描。ALL
: 全表扫描。
possible_keys
: MySQL可能使用的索引。key
: MySQL实际使用的索引。key_len
: 索引的长度。rows
: MySQL估计需要扫描的行数。Extra
: 额外信息,例如Using index
(使用了索引覆盖),Using where
(需要使用WHERE条件过滤) ,Using temporary
(使用了临时表),Using filesort
(使用了文件排序)。
目标: 尽量让
type
达到ref
或range
级别,rows
越少越好,避免Using temporary
和Using filesort
。
例子:索引覆盖
假设有一个表users
,包含id
, username
, email
三个字段,id
是主键,username
建了索引。
-
没有索引覆盖:
SELECT username, email FROM users WHERE username = 'test';
这条语句会先通过
username
索引找到匹配的行,然后回表查询email
字段。 -
索引覆盖:
SELECT username FROM users WHERE username = 'test';
这条语句只需要通过
username
索引就可以获取到所有需要的数据,不需要回表查询。为了实现索引覆盖,可以创建一个包含所有需要查询的列的组合索引:
ALTER TABLE users ADD INDEX idx_username_email (username, email);
这样,即使查询
SELECT username, email FROM users WHERE username = 'test'
,也可以利用索引覆盖,避免回表查询。
第三部分:SQL语句的写法:精益求精
除了索引之外,SQL语句的写法也会影响查询效率。
3.1 分页查询优化
分页查询是常见的需求,但如果数据量很大,分页查询可能会很慢。
-
普通分页:
SELECT * FROM orders LIMIT 100000, 10;
这条语句会扫描
100010
行数据,然后丢弃前100000
行,只返回最后10
行。如果100000
很大,效率会很低。 -
优化方法一:利用索引
如果
id
是主键,可以利用id
进行优化:SELECT * FROM orders WHERE id > (SELECT id FROM orders LIMIT 100000, 1) LIMIT 10;
这条语句会先找到第
100000
行的id
,然后查询id
大于该值的10
行数据。 -
优化方法二:书签记录
在页面上记录上次查询的最大
id
,下次查询时直接从该id
开始:SELECT * FROM orders WHERE id > last_id LIMIT 10;
这种方法适用于用户按照
id
顺序浏览数据的情况。 -
优化方法三:延迟关联/子查询优化
如果查询的列很多,回表查询的代价很高,可以考虑使用延迟关联:
SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders LIMIT 100000, 10) AS sub ON o.id = sub.id;
这条语句先通过子查询获取
id
,然后通过id
关联查询其他列。
3.2 JOIN优化
JOIN操作是SQL查询中最耗时的操作之一。
- 选择合适的JOIN类型:
INNER JOIN
,LEFT JOIN
,RIGHT JOIN
,FULL OUTER JOIN
。不同的JOIN类型适用于不同的场景。 - 小表驱动大表: MySQL的JOIN算法通常是Nested-Loop Join,小表作为外层循环,大表作为内层循环。所以,应该尽量让小表驱动大表。
- JOIN列要有索引: JOIN操作需要比较列的值,索引可以加快比较速度。
- 避免笛卡尔积: 如果JOIN条件不正确,可能会导致笛卡尔积,结果集会非常大,效率会很低。
3.3 子查询优化
子查询可以简化SQL语句,但也可能导致性能问题。
- 尽量将子查询转换为JOIN: 很多情况下,子查询可以用JOIN代替。JOIN通常比子查询更高效。
- 避免在SELECT列表中使用子查询: SELECT列表中的子查询会为每一行数据执行一次,效率很低。
- 使用EXISTS代替IN: 如果子查询返回的结果集很大,使用EXISTS比IN更高效。
3.4 UNION优化
UNION操作用于合并多个SELECT语句的结果集。
- UNION ALL vs UNION: UNION ALL会直接合并结果集,不去重。UNION会去重,需要进行排序和比较,效率较低。如果不需要去重,应该使用UNION ALL。
- 尽量减少UNION的数量: UNION的数量越多,性能越差。
第四部分:一些小技巧:锦上添花
- 使用预编译语句 (Prepared Statements): 预编译语句可以减少SQL解析和编译的次数,提高执行效率。尤其是在需要重复执行相同SQL语句的情况下。
- 批量插入数据: 批量插入数据比逐条插入数据效率更高。
- 禁用查询缓存: 如果数据变化频繁,查询缓存可能会导致数据不一致。可以禁用查询缓存。 (MySQL 8.0 已经移除了查询缓存)
- 定期优化表: 使用
OPTIMIZE TABLE
命令可以优化表结构,减少碎片,提高查询效率。 - 分析慢查询日志: MySQL提供了慢查询日志,可以记录执行时间超过指定阈值的SQL语句。通过分析慢查询日志,可以找到需要优化的SQL语句。
第五部分:可读性:优雅的SQL是怎样炼成的
写出高效的SQL很重要,但写出可读性强的SQL同样重要。可读性强的SQL更容易理解、维护和调试。
- 使用合适的缩进和换行: 让SQL语句的结构清晰明了。
- 使用有意义的别名: 让SQL语句更容易理解。
- 添加注释: 解释SQL语句的意图和逻辑。
- 使用一致的命名规范: 例如,表名、列名、索引名都应该遵循统一的命名规范。
- 拆分复杂的SQL语句: 将复杂的SQL语句拆分成多个简单的SQL语句,用临时表或变量存储中间结果。
例子:一个复杂SQL语句的优化过程
假设有一个需求:查询所有订单金额大于100的用户的用户名和邮箱,并按照订单总金额降序排列。
-
初始SQL:
SELECT u.username, u.email FROM users u WHERE u.id IN (SELECT o.customer_id FROM orders o WHERE o.amount > 100) ORDER BY (SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = u.id) DESC;
这条SQL语句使用了子查询和相关子查询,效率很低。
-
第一次优化:将子查询转换为JOIN
SELECT u.username, u.email FROM users u INNER JOIN (SELECT DISTINCT customer_id FROM orders WHERE amount > 100) o ON u.id = o.customer_id ORDER BY (SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = u.id) DESC;
将
IN
子查询转换为JOIN
,效率有所提高。 -
第二次优化:移除相关子查询
SELECT u.username, u.email, SUM(o.amount) AS total_amount FROM users u INNER JOIN orders o ON u.id = o.customer_id WHERE o.amount > 100 GROUP BY u.id ORDER BY total_amount DESC;
将
ORDER BY
中的相关子查询移除,使用GROUP BY
和SUM()
函数计算总金额。 -
第三次优化:添加索引
在
orders
表的customer_id
和amount
列上添加索引。ALTER TABLE orders ADD INDEX idx_customer_id_amount (customer_id, amount);
这条SQL语句已经比较优化了,但还可以根据实际情况进行调整。
总结
SQL优化是一个持续学习和实践的过程。没有万能的解决方案,只有根据具体情况选择最合适的优化方法。重要的是理解SQL的执行原理,善用工具,不断尝试和总结。
记住,写出高效且可读性强的SQL,不仅能提高系统性能,还能提升你的个人价值!
今天的分享就到这里,感谢各位的观看!希望大家都能成为SQL优化大师! 下次再见!