你好,数据库的使用者们!今天,我们来聊聊一个在数据库性能调优中至关重要,却又常常被忽视的“藏宝图”——执行计划。无论你使用的是哪种数据库,理解执行计划都是通往高性能SQL的必经之路。今天,我们就以阿里云的云原生数据库PolarDB为例,来一场深入浅出的执行计划解读之旅。
想象一下,你给数据库下达了一条SQL指令,比如“帮我找出上个月所有购买了某款产品且来自北京的用户信息”。数据库可不是一拍脑袋就去翻箱倒柜,它内部有一个非常聪明的“查询优化器”。这个优化器会像一位经验丰富的图书馆管理员,面对你的请求,它会快速构思出几种不同的查找方案:是先按“购买时间”这个索引快速定位到上个月的记录,再从中筛选“北京”的用户?还是先用“城市”索引找到所有北京用户,再核对他们的购买记录?抑或是干脆把整个“用户-订单”大表从头到尾扫描一遍?
这个被优化器最终选定的、最有效率的“查找方案”,就是执行计划。解读它,你就能看透数据库执行SQL的每一个心思和步骤,从而找到性能瓶颈的症结所在。
一、获取执行计划:让数据库交出它的“作战方案”
在PolarDB(这里我们以PolarDB for MySQL 兼容版为例)中,获取执行计划最常用的方法是使用 EXPLAIN 命令。只需在你要分析的SQL语句前加上 EXPLAIN 或 EXPLAIN FORMAT=JSON 即可。
示例1:获取一个简单查询的执行计划
-- 假设我们有一个订单表 orders
-- 我们先获取一个简单查询的文本格式执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND order_date > '2023-10-01';
-- 输出可能类似于:
-- +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
-- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
-- +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
-- | 1 | SIMPLE | orders | NULL | ref | idx_user | idx_user| 4 | const | 23 | 33.33 | Using where |
-- +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
注释:EXPLAIN 命令并没有真正执行后面的 SELECT 语句,它只是让优化器展示其计划。type=ref 表示使用了非唯一索引扫描,key=idx_user 显示使用了哪个索引,rows=23 是优化器预估要检查的行数,这是一个非常重要的效率指标。
示例2:获取更详细的JSON格式执行计划
-- JSON格式包含更丰富的信息,是深度分析的首选
EXPLAIN FORMAT=JSON
SELECT o.order_id, o.amount, u.user_name
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.city = '北京' AND o.status = 'completed'
ORDER BY o.order_date DESC
LIMIT 10;
注释:执行后,你会得到一个结构化的JSON对象,其中包含了完整的执行过程树、每一步的成本估算、访问方法、连接类型等详细信息。虽然看起来复杂,但它是性能分析的“富矿”。
二、解读核心字段:看懂执行计划的“密码”
面对 EXPLAIN 的输出,我们需要聚焦几个关键字段。这里我们结合一个稍复杂的例子来解读。
示例3:分析一个多表关联查询
-- 创建示例表结构(假设)
-- CREATE TABLE users (user_id INT PRIMARY KEY, user_name VARCHAR(100), city VARCHAR(50));
-- CREATE TABLE orders (order_id INT PRIMARY KEY, user_id INT, amount DECIMAL(10,2), city VARCHAR(50), order_date DATE);
-- CREATE INDEX idx_orders_user ON orders(user_id);
-- CREATE INDEX idx_orders_city_date ON orders(city, order_date);
EXPLAIN
SELECT u.user_name, SUM(o.amount) as total_amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE o.city = '上海'
AND o.order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY u.user_id, u.user_name
HAVING total_amount > 10000
ORDER BY total_amount DESC;
假设执行计划输出有两行(代表两个表的访问)。我们来解读关键列:
- id: 执行序号。id相同,执行顺序从上到下(如关联查询);id不同,序号越大越先执行(如子查询)。
- select_type: 查询类型。
SIMPLE(简单查询)、PRIMARY(最外层查询)、SUBQUERY(子查询)等。本例中驱动表(通常是users)可能是SIMPLE,被驱动表(orders)的select_type在关联查询中通常也是SIMPLE(对于JOIN)。 - table: 当前步骤访问的表。
- type(访问类型): 这是衡量性能最重要的指标之一! 从优到劣常见的有:
system/const: 通过主键或唯一索引一次就找到,最优。eq_ref: 关联查询中,使用主键或唯一索引进行关联。ref: 使用非唯一索引进行查找。我们示例中o.city上的条件可能用到此类型(如果索引是(city, order_date))。range: 索引范围扫描,如BETWEEN,>,<等。我们的o.order_date条件在联合索引中可能体现为范围扫描。index: 全索引扫描(比全表扫描快,因为只读索引)。ALL: 全表扫描,最差情况,需要极力避免。
- possible_keys: 可能用到的索引。
- key: 实际用到的索引。如果为NULL,则未用索引。在我们的例子中,
orders表应该会用到idx_orders_city_date索引。 - key_len: 使用的索引长度。可以判断是否充分利用了复合索引。例如
idx_orders_city_date (city, order_date),如果key_len只等于city的长度,说明order_date并未完全用于索引查找,而是用于过滤。 - rows: 优化器预估需要扫描的行数。结合
type看,如果rows很大但type很差(如ALL),就是性能警报。 - Extra: 额外信息,包含大量重要细节。
Using where: 表示在存储引擎检索行后,服务器层再次进行了过滤。如果type是index或range,这很正常;如果是ALL,则说明筛选条件没用到索引。Using index: 表示查询使用了覆盖索引,无需回表,性能极佳。Using temporary: 表示需要创建临时表来处理查询(如GROUP BY、ORDER BY),如果数据量大可能影响性能。Using filesort: 表示无法利用索引完成排序,需要额外的排序步骤。我们的示例中ORDER BY total_amount很可能导致此情况。Using join buffer: 表示连接查询使用了连接缓冲区,通常发生在没有理想索引用于关联时。
关联技术:覆盖索引
为了优化上述示例,避免 Using filesort 和 Using temporary,一个高级技巧是使用覆盖索引。覆盖索引是指一个索引包含了查询所需的所有字段,使得查询只需扫描索引而无需回表取数据。
-- 如果我们为上述查询创建这样一个覆盖索引
-- CREATE INDEX idx_covering ON orders(city, order_date, user_id, amount);
-- 那么对于 orders 表的过滤和聚合部分,执行计划中的 Extra 字段可能会出现 ‘Using index’, 效率大幅提升。
-- 但注意,GROUP BY 和 ORDER BY 涉及的是 user 表的字段和聚合结果,此索引主要优化了 orders 表的扫描部分。
三、实战优化案例:从计划到性能提升
光说不练假把式,我们来看一个真实的优化案例。
问题SQL:一个分页查询在数据量变大后越来越慢。
-- 原始慢SQL
SELECT * FROM article WHERE category_id = 10 AND status = 'published' ORDER BY publish_time DESC LIMIT 100000, 20;
获取并分析计划:
EXPLAIN FORMAT=JSON
SELECT * FROM article WHERE category_id = 10 AND status = 'published' ORDER BY publish_time DESC LIMIT 100000, 20;
计划分析:假设执行计划显示 type=index(使用了 (category_id, status, publish_time) 这个索引),但 rows 预估非常大,并且 Extra 中没有 Using where。这意味着,虽然走了索引排序,但它需要沿着索引扫描“100000+20”条记录,然后抛弃前10万条。LIMIT M, N 中的大偏移量 M 是罪魁祸首。
优化方案:使用“延迟关联”或“书签法”优化大偏移量分页。
-- 优化后的SQL
SELECT a.*
FROM article a
INNER JOIN (
SELECT id -- 只选择主键
FROM article
WHERE category_id = 10 AND status = 'published'
ORDER BY publish_time DESC
LIMIT 100000, 20
) AS tmp ON a.id = tmp.id -- 通过主键快速回表获取20行完整数据
ORDER BY a.publish_time DESC; -- 维持顺序
优化后计划分析:子查询 SELECT id ... 可以利用覆盖索引((category_id, status, publish_time, id)),只需要扫描索引的100020条记录,但不需要回表,速度较快。拿到20个目标id后,外层查询通过主键id快速回表获取完整数据行。整个过程避免了大量无用的回表操作,性能提升可能达到几个数量级。
四、PolarDB的特色与进阶分析
PolarDB作为云原生数据库,在执行计划方面除了提供标准MySQL的 EXPLAIN 功能外,还有一些增强特性:
- 执行计划管理:PolarDB可以绑定执行计划,防止因统计信息变化等原因导致优化器选择更差的计划,非常适合稳定核心业务SQL的性能。
- 更准确的代价估算:PolarDB的优化器针对云上硬件和存储特点进行了优化,其代价模型可能更准确,
rows等预估值的可靠性更高。 - 并行查询:对于复杂的分析型查询,PolarDB可以生成并行执行计划,利用多核能力加速处理。
EXPLAIN输出中可能会显示Parallel相关的信息。 - 智能诊断:在PolarDB的控制台或性能洞察功能中,它可能直接对慢SQL的执行计划进行可视化分析和优化建议,降低了人工解读的门槛。
应用场景:
- 日常慢SQL排查:当监控到慢查询时,第一时间通过
EXPLAIN分析其执行计划。 - SQL上线前评审:在开发阶段,对核心或复杂的SQL进行执行计划审查,预防性能问题。
- 索引优化:根据执行计划中的
type、key、Extra等信息,判断现有索引是否有效,并设计更优的索引。 - 数据库升级或迁移验证:在版本升级或数据迁移后,对比关键SQL的执行计划是否发生变化,确保性能稳定。
技术优缺点:
- 优点:
- 洞察本质:直接揭示数据库内部工作方式,是性能调优最根本的手段。
- 成本低廉:
EXPLAIN命令几乎不消耗资源。 - 通用性强:解读执行计划的技能可迁移到大多数关系型数据库。
- 缺点:
- 学习曲线:需要记忆和理解大量专业术语和字段含义。
- 基于估算:
EXPLAIN显示的是优化器“认为”的计划和成本,并非实际运行时数据。极端情况下,估算不准可能导致优化器选错计划。 - 静态分析:传统的
EXPLAIN无法反映运行时资源竞争、锁等待等情况。需要结合SHOW PROFILE、performance_schema或EXPLAIN ANALYZE(PolarDB PostgreSQL兼容版支持)进行动态分析。
注意事项:
- 结合统计信息:执行计划依赖于表的统计信息(如行数、索引区分度)。务必确保
ANALYZE TABLE定期执行,保持统计信息新鲜。 - 关注数据分布:优化器的选择严重依赖数据分布。测试环境与生产环境数据量、数据分布不同,可能导致执行计划截然不同。
- 不要过度优化:索引不是越多越好。每个索引都会增加写操作成本。优化应针对核心瓶颈,追求平衡。
- 善用格式化工具:对于复杂的
EXPLAIN FORMAT=JSON输出,可以借助一些在线格式化工具或IDE插件,使其更易读。
文章总结:
执行计划就像数据库医生手中的X光片,它能清晰照出SQL语句的“骨骼”与“脉络”。掌握PolarDB执行计划的解读,意味着你获得了主动掌控SQL性能的钥匙。从识别全表扫描(ALL)的警报,到理解索引覆盖(Using index)的喜悦,再到通过优化连接方式和重写SQL来扭转乾坤,每一步都建立在扎实的解读能力之上。记住,调优不是玄学,而是基于执行计划这份“蓝图”的科学分析和实践。多查看、多思考、多实践,你就能逐渐培养出对SQL性能的敏锐直觉,让数据库的每一次查询都高效如飞。
评论