各位观众老爷,大家好!我是今天的主讲人,江湖人称“SQL优化小霸王”。今天咱们来聊聊MySQL里一个非常强大的武器——EXPLAIN ANALYZE
,它可以让你像侦探一样,揪出SQL语句里的性能瓶颈。
一、先来点前戏:EXPLAIN
熟悉一下
在深入EXPLAIN ANALYZE
之前,我们先快速回顾一下老朋友EXPLAIN
。 EXPLAIN
命令可以显示MySQL如何执行你的查询语句,它会告诉你MySQL将使用哪些索引,连接类型,以及扫描的数据量等等。但是EXPLAIN
有个问题,它给你的只是估计的执行计划。
EXPLAIN SELECT * FROM orders WHERE customer_id = 123 AND order_date > '2023-01-01';
执行完这条语句,你会得到类似下面的输出(简化版):
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | orders | ref | customer_id | customer_id | 4 | const | 100 | Using where |
这些列分别代表什么呢?
- id: 查询的标识符。如果一个查询包含多个SELECT语句(子查询等),则会有多个id。
- select_type: 查询的类型,例如SIMPLE(简单查询), PRIMARY (最外层查询), SUBQUERY (子查询)等。
- table: 查询涉及的表名。
- type: MySQL决定如何查找表中的行。这是一个非常重要的列,它显示了访问类型,从最佳到最差依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
。 我们要尽量避免ALL
(全表扫描)。 - possible_keys: 可能用于查找行的索引。
- key: MySQL实际选择使用的索引。
- key_len: 使用的索引的长度。
- ref: 将哪个列或常量与索引进行比较。
- rows: MySQL估计需要扫描的行数。 这是一个估计值,并非真实值。
- Extra: 包含有关MySQL如何解决查询的额外信息。例如,
Using index
表示使用了覆盖索引,Using where
表示使用了WHERE子句过滤行,Using temporary
表示使用了临时表,Using filesort
表示使用了文件排序。Using temporary
和Using filesort
往往是性能瓶颈的信号。
EXPLAIN
的缺点在于,它只是告诉你MySQL计划怎么做,而不是实际怎么做的。rows
列只是一个估计值,实际执行时可能偏差很大。这就是EXPLAIN ANALYZE
大显身手的地方。
二、EXPLAIN ANALYZE
闪亮登场!
EXPLAIN ANALYZE
是MySQL 8.0.18版本引入的,它会真正执行你的SQL语句,并提供实际的执行计划和性能指标。这意味着你可以看到每个步骤花费了多少时间,扫描了多少行,等等。这对于识别性能瓶颈非常有帮助。
语法:
EXPLAIN ANALYZE SELECT ... FROM ... WHERE ...;
就是这么简单!把你的SELECT语句放在 EXPLAIN ANALYZE
后面就行了。
例子:
假设我们有一个products
表和一个orders
表:
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
category VARCHAR(255),
price DECIMAL(10, 2)
);
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT,
customer_id INT,
order_date DATE,
quantity INT,
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- 插入一些测试数据
INSERT INTO products (name, category, price) VALUES
('Laptop', 'Electronics', 1200.00),
('Mouse', 'Electronics', 25.00),
('Keyboard', 'Electronics', 75.00),
('T-shirt', 'Clothing', 20.00),
('Jeans', 'Clothing', 50.00);
INSERT INTO orders (product_id, customer_id, order_date, quantity) VALUES
(1, 101, '2023-01-15', 1),
(2, 101, '2023-02-01', 2),
(3, 102, '2023-02-10', 1),
(4, 103, '2023-03-01', 3),
(5, 102, '2023-03-15', 1),
(1, 104, '2023-04-01', 1),
(2, 103, '2023-04-10', 2);
现在,我们执行一个简单的查询,并用EXPLAIN ANALYZE
分析它:
EXPLAIN ANALYZE SELECT o.id, p.name
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE o.customer_id = 101;
执行后,你会得到一大段输出,别慌,我们来解读一下:
-> Filter: (o.customer_id = 101) (cost=1.25 rows=1) (actual time=0.012..0.013 rows=2 loops=1)
-> Nested loop inner join (cost=1.10 rows=1) (actual time=0.007..0.011 rows=2 loops=1)
-> Table scan on o (cost=0.00 rows=7) (actual time=0.002..0.003 rows=7 loops=1)
-> Single-row index lookup on p using PRIMARY (id=o.product_id) (cost=0.18 rows=1) (actual time=0.003..0.003 rows=1 loops=7)
解读输出:
-
-> Filter: (o.customer_id = 101) (cost=1.25 rows=1) (actual time=0.012..0.013 rows=2 loops=1)
Filter
表示使用了WHERE子句进行过滤。(o.customer_id = 101)
指明了过滤条件。cost=1.25
MySQL的优化器估算的成本。 成本越低越好。rows=1
MySQL估计会返回1行数据。actual time=0.012..0.013
实际执行时间,单位是秒。0.012
是第一次找到符合条件的行的时间,0.013
是找到所有符合条件的行的时间。rows=2
实际返回了2行数据。 注意这里和估计的rows=1
不一样了。loops=1
循环次数。
-
-> Nested loop inner join (cost=1.10 rows=1) (actual time=0.007..0.011 rows=2 loops=1)
Nested loop inner join
表示使用了嵌套循环连接。cost=1.10
估算成本。rows=1
估计行数。actual time=0.007..0.011
实际执行时间。rows=2
实际行数。loops=1
循环次数。
-
-> Table scan on o (cost=0.00 rows=7) (actual time=0.002..0.003 rows=7 loops=1)
Table scan on o
表示对orders
表进行了全表扫描。 这通常不是一个好现象,尤其是当表很大的时候。cost=0.00
估算成本。rows=7
估计行数和实际行数都是7,说明orders
表总共只有7行数据。actual time=0.002..0.003
实际执行时间。loops=1
循环次数。
-
-> Single-row index lookup on p using PRIMARY (id=o.product_id) (cost=0.18 rows=1) (actual time=0.003..0.003 rows=1 loops=7)
Single-row index lookup on p using PRIMARY (id=o.product_id)
表示在products
表上使用了主键索引进行单行查找。 这是一个高效的操作。cost=0.18
估算成本。rows=1
估计行数和实际行数都是1。actual time=0.003..0.003
实际执行时间。loops=7
循环次数是7,因为orders
表有7行数据,需要对每一行都在products
表上进行查找。
总结:
从上面的分析可以看出,这个查询的瓶颈在于对orders
表的全表扫描。 为了优化这个查询,我们可以给orders
表的customer_id
列添加索引:
CREATE INDEX idx_customer_id ON orders (customer_id);
然后再次执行EXPLAIN ANALYZE
:
EXPLAIN ANALYZE SELECT o.id, p.name
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE o.customer_id = 101;
这次的输出可能会变成这样:
-> Filter: (o.customer_id = 101) (cost=0.35 rows=1) (actual time=0.006..0.007 rows=2 loops=1)
-> Nested loop inner join (cost=0.20 rows=1) (actual time=0.002..0.006 rows=2 loops=1)
-> Index lookup on o using idx_customer_id (customer_id=101) (cost=0.15 rows=1) (actual time=0.001..0.002 rows=2 loops=1)
-> Single-row index lookup on p using PRIMARY (id=o.product_id) (cost=0.18 rows=1) (actual time=0.001..0.001 rows=1 loops=2)
变化:
Table scan on o
变成了Index lookup on o using idx_customer_id (customer_id=101)
,这意味着我们成功地使用了索引,避免了全表扫描。actual time
也显著降低了。
三、EXPLAIN ANALYZE
的进阶技巧
-
关注
actual time
: 这是最重要的指标。 找出actual time
最高的步骤,那就是你的性能瓶颈。 -
比较
rows
的估计值和实际值: 如果估计值和实际值相差很大,说明MySQL的统计信息可能不准确。你可以尝试更新表的统计信息:ANALYZE TABLE orders;
-
注意
Using temporary
和Using filesort
: 这两个Extra
信息通常意味着性能问题。 尽量避免使用临时表和文件排序。 -
结合
SHOW WARNINGS
: 在执行EXPLAIN
后,可以执行SHOW WARNINGS
来查看MySQL的优化器对你的SQL语句做了哪些修改。 这可以帮助你更好地理解MySQL的行为。EXPLAIN ANALYZE
本身会显示更详细的信息,但在某些情况下,SHOW WARNINGS
仍然有用。 -
配合慢查询日志:
EXPLAIN ANALYZE
可以帮助你分析慢查询日志中记录的SQL语句。 找到执行时间长的SQL语句,然后用EXPLAIN ANALYZE
分析它们,找出性能瓶颈。
四、一个更复杂的例子
假设我们还有一个reviews
表,记录了用户对产品的评价:
CREATE TABLE reviews (
id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT,
customer_id INT,
rating INT,
comment TEXT,
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- 插入一些测试数据
INSERT INTO reviews (product_id, customer_id, rating, comment) VALUES
(1, 101, 5, 'Great laptop!'),
(1, 102, 4, 'Good value for the price.'),
(2, 101, 3, 'Okay mouse.'),
(3, 103, 5, 'Excellent keyboard!'),
(4, 102, 2, 'Too expensive for a T-shirt.');
现在,我们执行一个更复杂的查询,找出所有购买了“Laptop”的顾客的评价:
EXPLAIN ANALYZE SELECT r.rating, r.comment
FROM orders o
JOIN products p ON o.product_id = p.id
JOIN reviews r ON o.product_id = r.product_id AND o.customer_id = r.customer_id
WHERE p.name = 'Laptop';
执行后,分析输出结果,找出潜在的性能瓶颈,并尝试优化。 例如,你可能会发现需要在reviews
表上创建复合索引:
CREATE INDEX idx_product_customer ON reviews (product_id, customer_id);
五、EXPLAIN ANALYZE
的局限性
- 只适用于SELECT语句:
EXPLAIN ANALYZE
只能用于SELECT语句,不能用于INSERT, UPDATE, DELETE等语句。 - 会真正执行SQL语句:
EXPLAIN ANALYZE
会真正执行你的SQL语句,所以如果你的SQL语句会修改数据,请谨慎使用。 在测试环境中使用是更安全的做法。 - 需要MySQL 8.0.18或更高版本:
EXPLAIN ANALYZE
是MySQL 8.0.18版本引入的,所以如果你使用的是旧版本,你需要升级MySQL。 - 输出信息量大:
EXPLAIN ANALYZE
的输出信息量很大,需要一定的经验才能理解。
六、总结
EXPLAIN ANALYZE
是一个非常强大的SQL优化工具,可以帮助你找出SQL语句的性能瓶颈,并进行优化。 但是,它也有一些局限性,需要谨慎使用。 希望今天的讲座能帮助你更好地理解和使用EXPLAIN ANALYZE
,成为一名SQL优化高手!
记住,SQL优化没有银弹。 EXPLAIN ANALYZE
只是一个工具,你需要结合实际情况,不断尝试和调整,才能找到最佳的优化方案。
好了,今天的讲座就到这里。 感谢大家的观看! 下次有机会再跟大家分享更多的SQL优化技巧。 祝大家写出又快又好的SQL语句!